Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AGENTS.md
# Sonic

Guidance for coding agents working in this repository. Keep this aligned with `CLAUDE.md`.
Guidance for coding agents working in this repository.

## Mission

Expand Down Expand Up @@ -54,7 +54,7 @@ lib/themes.js theme registry and palette helpers
- keep docs in sync with real behavior
- prefer YouTube-first improvements over speculative multi-service work
- if you add another service, explain the API/auth/legal model in `README.md`
- keep project-level `CLAUDE.md` and `AGENTS.md` versioned in git; do not hide them in `.gitignore`
- keep the project-level `AGENTS.md` versioned in git; do not hide it in `.gitignore`
- if you touch theme work, keep the identity strong across the full TUI, not just the header

## Inspiration
Expand Down
114 changes: 0 additions & 114 deletions CLAUDE.md

This file was deleted.

108 changes: 81 additions & 27 deletions bin/sonic.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import readline from 'node:readline';
import { readFileSync } from 'node:fs';
import chalk from 'chalk';

import { search, SOURCES } from '../lib/search.js';
import { search, SOURCES, parseSourceUrl, resolveUrl } from '../lib/search.js';
import { Player } from '../lib/player.js';
import { UI } from '../lib/ui.js';
import {
Expand Down Expand Up @@ -83,6 +83,8 @@ if (args.includes('--mc') || args.includes('--mixcloud')) {
source = 'mixcloud';
}

const LINK_TRACK_LIMIT = 100;

let pickerTab = 'vibes';
let vibesCursor = 0;
let activityCursor = 0;
Expand Down Expand Up @@ -319,21 +321,14 @@ function showRecents() {
});
}

async function doSearch(query) {
currentQuery = query;
state = 'searching';
// Runs an async task behind the animated loader. Never throws: the caller
// decides how to present success and failure.
async function runWithLoader(tips, task) {
ui.hideCursor();
enableRawKeys();
ui.clear();
process.stdout.write(ui.header(source));

const tips = [
`searching ${source}...`,
'finding the best mixes...',
'curating your vibe...',
'almost there...'
];

let frame = 0;
let tipIdx = 0;
const tipSwap = setInterval(() => {
Expand All @@ -346,28 +341,87 @@ async function doSearch(query) {
}, 80);

try {
const searchLimit = store.config.searchLimit;
const foundResults = await search(query, { source, limit: searchLimit });
clearInterval(spin);
clearInterval(tipSwap);

setResultsView(foundResults, {
title: 'search results',
subtitle: `${source} · ${query}`,
kind: 'search',
emptyMessage: `no ${source} results for “${query}”.`,
footer: 'enter play · f favorite · / search · b back · m source · t theme · d mode · q quit'
});
const value = await task();
return { ok: true, value };
} catch (err) {
return { ok: false, error: err };
} finally {
clearInterval(spin);
clearInterval(tipSwap);
ui.clear();
process.stdout.write(ui.header(source));
process.stdout.write(`\n ${chalk.red('search failed:')} ${err.message}\n`);
promptSearch();
}
}

function showLoadFailure(label, err) {
ui.clear();
process.stdout.write(ui.header(source));
process.stdout.write(`\n ${chalk.red(label)} ${err.message}\n`);
promptSearch();
}

async function doSearch(query) {
const link = parseSourceUrl(query);
if (link) {
await doOpenLink(query, link);
return;
}

currentQuery = query;
state = 'searching';

const outcome = await runWithLoader([
`searching ${source}...`,
'finding the best mixes...',
'curating your vibe...',
'almost there...'
], () => search(query, { source, limit: store.config.searchLimit }));

if (!outcome.ok) {
showLoadFailure('search failed:', outcome.error);
return;
}

setResultsView(outcome.value, {
title: 'search results',
subtitle: `${source} · ${query}`,
kind: 'search',
emptyMessage: `no ${source} results for “${query}”.`,
footer: 'enter play · f favorite · / search · b back · m source · t theme · d mode · q quit'
});
}

async function doOpenLink(rawUrl, link) {
currentQuery = null;
state = 'searching';
source = link.source;

const outcome = await runWithLoader([
`opening ${link.source} link...`,
'resolving track info...',
'almost there...'
], () => resolveUrl(rawUrl, { limit: LINK_TRACK_LIMIT }));

if (!outcome.ok) {
showLoadFailure('link failed:', outcome.error);
return;
}

const { tracks, kind, note } = outcome.value;
const heading = kind === 'playlist' ? 'playlist' : 'linked track';

let subtitle = `${link.source} · ${tracks.length} track${tracks.length === 1 ? '' : 's'}`;
if (note) {
subtitle = `${subtitle} · ${note}`;
}

setResultsView(tracks, {
title: heading,
subtitle,
kind: 'link',
emptyMessage: 'that link resolved to no playable tracks.',
footer: 'enter play · f favorite · / search · b back · m source · t theme · d mode · q quit'
});
}

async function playTrack(track) {
const hydrated = hydrateTrack(track);
currentTrack = hydrated;
Expand Down
Loading
Loading