Verified against live ReClip: POST /api/download with title:"" → a hash filename
(b5d04adc86.mp3); with title:"Me at the zoo" → "Me at the zoo.mp3". So ReClip
names the file from the title WE send (falling back to a hash) — it does not
self-name. The title is mandatory, which means a metadata pass is required.
Back to two phases:
1. metadata — fetch each item's /api/info (title + validity), keep survivors, skip
errors.
2. download — download each survivor passing its title, so files land with real
names; skip download errors.
Keeps the exact-urls[] input (Mix playlists can't drift) and the one-request-per-
item download. Progress is two counters again (Titles + Download); UI shows two
bars. ~2 requests/item is inherent to needing the title (per the user's call:
correctness over speed).
Verified two-phase filtering + title passthrough + skip-on-error with a mock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A YouTube Mix/radio playlist (list=RD…) returns a different set of items on every
/api/playlist call (observed 779 / 1485 / 529 for the same URL). The job used to
re-expand the playlist server-side, so it would download a different list than the
count shown on the decision screen.
The panel now passes the already-expanded `urls[]` into the job, and the executor
uses them verbatim (falling back to expanding `url` only when no list is given).
The job downloads exactly what you decided on. Endpoint takes `urls[]` (stored as
inputs.urls) or `url`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The job's two phases were a misread — the "count" phase is the client-side
playlist expansion (for the inline-vs-job decision, already done in the panel).
The job itself is just one download request per item.
Dropped the in-job metadata pass entirely:
- reclip-client: reclipDownloadOne no longer prefetches /api/info for a title —
ReClip names the file from the video title itself, so it's a single request
per item.
- execute-download: one phase — expand the playlist, then /api/download each url,
skip failures. Progress is a single { done, failed, total, current } counter
(no meta/dl split); ~2× faster and downloads start right after expansion.
- UI (DownloadJobDetail + panel JobView): one "Downloaded" bar instead of two.
Verified: every item is attempted directly (no /api/info gate), skip-on-error
counts correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Front half of the download-job feature.
Panel (VideoDownloadPanel): after Fetch expands a playlist and the count is known,
a decision screen — "Found N items" → pick Audio/Video + subfolder → "Download all
as a job" (POST /jobs/download), or "fetch inline to pick individually" (the
existing card grid). A single video still goes straight to the inline card. The
job phase shows live two-phase progress (polled from the job) + a "View in Jobs"
link; it notes the job runs server-side so closing the panel is fine, and it
refreshes the browser as each file lands.
/jobs (DownloadJobDetail + JobsPage dispatch): a `download` job renders a compact
two-phase readout — Metadata and Download bars (processed/total, found/skipped and
saved/failed) + the current item — polled from the job's progress, with a Stop.
Executor tweak: phase-1 meta.done now counts kept (not processed) so both phases
read the same `(done+failed)/total`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the download model around a format choice (video = ReClip video, audio =
audioOnly) instead of a global "extract audio" toggle:
- Input phase is just URL + Fetch (dropped the audio checkbox).
- Each card tracks video + audio download state independently and shows a button
per format (Video only when ReClip reports video formats — audio-only sources
get just Audio).
- Playlist top actions: "All video" (when any item has video), "All audio", and
"Select".
- Select mode: each card shows Video/Audio checkboxes (pick one, both, or none per
item); a "Start download (N)" button runs the chosen set and shows a fake
progress bar (N/M count, no real byte progress). Bulk "All video/audio" reuse
the same progress bar.
- Single item = large card with the two format buttons, no bulk row.
VideoInfo gains `formats` so the client can tell video-capable from audio-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uses the resizable panel + container queries instead of a cramped vertical list:
- Single item → one large card (big 16:9 thumbnail, title, uploader·duration,
full-width download button).
- Playlist → a responsive grid (2 cols, 3 at @520px, 4 at @760px of panel width)
of compact cells, each with a position number badge and an overlay download/
status chip.
Loading skeletons, error, and per-cell download states (download → downloading →
saving → saved / retry) carry over.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The music dock was position:fixed, overlaying the bottom of the content, so the
nav dock needed a pile of hacks to dodge it: a hand-measured MUSIC_DOCK_HEIGHT
(72) constant, a presence flag, a translateY lift, and a matching hover-threshold
lift (via a ref) so it wouldn't slide away under the cursor. Fragile the moment
the music dock's height changed.
Now the dock is an in-flow bottom bar that reserves its own height:
- DashboardLayout is a flex column: the content region (flex-1, min-h-0) shrinks
when the music dock takes its space; MusicPlayerHost renders an in-flow bar
(shrink-0) instead of a fixed overlay.
- The nav Dock is absolute within the content region and measures its reveal/hide
boundary from that region's bottom edge (a boundaryRef) — so it always sits just
above whatever's at the bottom, music dock or not, with zero knowledge of it.
- Deleted MUSIC_DOCK_HEIGHT, musicDockPresent, the lift, and the transform hack.
Bonus: content at the very bottom is no longer hidden under the fixed dock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dock's X only reset local player state; the server's now-playing snapshot
survived, so a reload restored the dock. Closing now also DELETEs /music/now-playing
before clearing the queue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves the video-download UI from a modal into the same ephemeral side-panel slot
the file viewer uses (double-click a video), per request. It's driven by the
`download` search param (the target folder) + `downloadRoot`, exactly like the
existing view/play/chat ephemeral panels.
- layouts: singleDownloadLayout (files-download panel).
- VideoDownloadPanel (new, apps/FileBrowser): self-contained — reads the target
folder/root from the params, does its own useFilesAPI, prefetch + per-entry /
download-all flow (unchanged from the dialog), and bumps the shared
`files:refresh-signal` so the browser re-lists when a file lands. Header shows
a "Saving to <folder>" hint.
- useFileViewerPanels: register files-download (param → layout → panel + close),
add download/downloadRoot to the on-refresh cleanup keys.
- useFileBrowserApp: replace the showVideoDownload modal state with
openVideoDownload(), which sets the download param for the current folder+root.
- Toolbar + FileViewContainer trigger openVideoDownload(); drop the modal mount
and delete VideoDownloadDialog.tsx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the download-video dialog into a prefetch-then-download flow, mirroring
ReClip's own web UI. Still a pure proxy to ReClip (its yt-dlp); no downloader
logic moves to the platform.
Server (thin ReClip proxies alongside /download-video):
- POST /file-browser/video-info { url } → ReClip /api/info → { title, thumbnail, duration, uploader }
- POST /file-browser/video-playlist { url } → ReClip /api/playlist → { urls }
Both return { error } inline (200) so the client can render failures per-card.
UI (VideoDownloadDialog, now self-contained; useFileBrowserApp exposes `files`
and drops the old single-shot state/handler):
- Paste a URL → Fetch. A playlist URL (list=) expands via /video-playlist, then
each entry's /video-info is prefetched sequentially (ReClip does yt-dlp per
video), rendering a card (thumbnail, title, uploader, duration) that fills in
progressively.
- Per-entry Download, plus Download All when there's more than one; per-card
status (downloading → saving → saved / retry-on-error) via the existing
background job + poll.
- Playlists get an optional "subfolder you name" field (ReClip's /api/playlist
carries no playlist title); blank = current folder.
- Quality is always best (matches the mobile Share flow — no picker); the
audio-only toggle applies to the whole batch.
Verified ReClip's contract live: /api/info returns the metadata fields, and
/api/playlist returns { urls } (17 entries in ~1.2s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MusicBrowser and MusicDetail panels each kept their OWN local `manifest`
(and folder-listing) state, fetched in their own effects. The reindex (↻) button
lives in MusicBrowser and only refreshed its own copy — MusicDetail (the right
panel showing the tracklist/grid) never heard about it, so newly-indexed content
only appeared after navigating (which re-ran its effects).
Add a shared `music:resync` panel channel: when a reindex completes, MusicBrowser
bumps it to a fresh nonce, and both panels re-run their manifest/libraries/folder
fetches. In MusicDetail the fresh manifest object identity also re-triggers the
[cwd, manifest] listing effect, so the open album's meta/tracklist and any folder
grid refresh in place — no navigation required. Drops the browser's now-redundant
hand-refresh of its own state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AudioContext autoplay-unlock listener (pointerdown → ctx.resume()) was
persistent, so every click anywhere resumed the context — including after a
deliberate pause (pause = ctx.suspend()). Result: pause, click back on the page,
and the track resumed from its position while React's `playing` stayed false, so
the button showed "paused" and it took two clicks to actually stop it.
The gesture only needs to unlock the context ONCE; sticky activation lets
engine.play() resume it thereafter. Make the listener { once: true } so it can't
fight an intentional pause.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The engine is created once (mount effect), so its onIndex/onEndOfQueue callbacks
captured the first-render syncIndex/setPlaying. useGlobal's setData reads the
`data` from the render that created the setter when given a functional updater —
that's the INITIAL empty state. So when a track ended and the engine called
syncIndex(i) → setState(s => ({...s, index:i})), `s` was {queue:[], index:0}:
the queue got wiped, `current` went undefined, playback stopped and the dock
vanished. Manual track selection was unaffected because playQueue passes a plain
object (no stale `data` read) — which is why it seemed to work.
Route the two engine-invoked setters through refs kept current each render, so a
natural advance mirrors into the live state instead of the stale initial one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The player used a single <audio> element and advanced by swapping .src, which
re-fetches + re-buffers the next file — an audible gap between tracks. For a
continuous DJ mix (silence already trimmed at the edges) that's the whole
problem. Replace the <audio> element with a Web Audio engine that decodes each
track to an AudioBuffer and schedules the NEXT track's source to start() at the
exact AudioContext time the current track ends → sample-accurate, zero gap on
auto-advance.
- gapless-engine.ts (new): AudioContext + gain, LRU-capped decoded-buffer cache,
fetch-whole-file → decodeAudioData, boundary scheduling, seek/skip/play-pause
(pause = ctx.suspend so the clock + scheduled next freeze together), a
generation counter to invalidate stale onended/async, and a gesture unlock for
autoplay policy. Callbacks: onIndex/onTime/onEndOfQueue/onLoadingChange.
- MusicPlayerHost.tsx: drives the engine instead of an <audio> element. React
keeps the queue/index (useMusicPlayer); user actions (new album, jump, prev/
next) command the engine, and the engine's own natural advance mirrors back
via syncIndex WITHOUT restarting playback (that's what keeps the seam gapless).
Preserves restore/persist/heartbeat/album-nav/volume/heart; adds a decode
spinner on the play button (startup/skip has fetch+decode latency by nature).
- useMusicPlayer.ts: syncIndex() — set index without touching `playing`.
Trade-off (chosen deliberately over near-gapless preloading): true gapless
needs the whole next file decoded to PCM ahead of time (~200MB per 10-min
track), so the buffer cache is capped at 3. Verified the scheduler state
machine with a mocked AudioContext: next track scheduled at the current's exact
end sample, advance/promote/seek/skip/end-of-queue all correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Display-only panel at the top of the Get Lyrics run-task form (single file only):
- Backend: GET /file-browser/audio-meta?path= — ffprobe format tags + duration,
plus a second probe for embedded lyrics (USLT/SYLT/lyrics* keys, case-insensitive).
Returns { title, artist, duration, hasLyrics }; tolerant of missing tags/probe
failures.
- Client: files.audioMeta(path) + AudioMeta type in useFilesAPI.
- TaskRunnerModal: prefetch audioMeta for get-lyrics single-file runs (bypasses
the hasTrackPickers early-return, error-tolerant), and render AudioMetaPanel
above TaskInputForm — title/artist + a muted length + Lyrics: Yes/No chip,
filename fallback. Directories + other tasks unaffected (no panel, no probing).
Verified ffprobe logic on a real embedded-lyrics file. tsgo clean; formatted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`type: string` renders a single-line <input>; the new `type: text` renders a
resizable multi-line <textarea> (6 rows) — for pasted multi-line values like
lyrics. Checked before options/default so `text` always means free-form text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A heart button in the left panel header opens a Favorites view in the right
panel (coordinated via a new music:favorites panel channel). Grouped
Artists / Albums / Tracks, each row: cover thumb (indexed cover, icon fallback)
+ title/subtitle + a heart to un-favorite. Click an album/artist to navigate
the library there; click a track to play it in album context. Navigating
anywhere closes the view (cwd-change effect). Empty state prompts to heart
something.
useMusicFavorites now also returns the grouped `favorites`; shared gains the
channel + parseAlbumName.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add cursor-pointer to the player dock's buttons — track-info, prev/next (with
disabled:cursor-default), play/pause, mute, and close — and to MusicHeart (so
every heart across the player/tracklists gets it). The volume slider and SeekBar
already had it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bottom player dock's cover + title/artist is now a button — clicking it sets
the music:cwd channel to the playing album and navigates to /music, landing on
that album's tracklist (current track highlighted). Uses the global panel
channel + react-router navigate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Left panel (MusicBrowser):
- Fuzzy filter input (case-insensitive subsequence match) over the current
library/folder list; resets on navigation, with a clear button + "No matches".
- Reindex button (spins while running) → POST /music/reindex, then refreshes the
manifest and current listing.
Right panel album view (MusicDetail):
- Current track clearly highlighted: primary tint background + a Volume2 marker
replacing the track number + medium weight.
- Every track shows its artist (falling back to album artist) under the title,
and its duration on the right. Web Track type gains albumArtist + durationSec;
fmtDuration/fuzzyMatch added to shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Left panel (MusicBrowser) polish + a shared fix:
- Larger row text (text-base; "Music" heading text-lg).
- More space between rows (gap-1.5 + py-2).
- Leading thumbnail is the folder's indexed cover (its folder.jpg/cover.jpg,
server-compressed) with a Folder/Library icon fallback when there's none or
the image fails.
- Hide hidden files/folders (dotfiles like .claude) in the listing — applied to
MusicBrowser and the MusicDetail libraries/grid so neither panel shows them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web player kept no durable state, so a reload (and a return to /music,
whose cwd resets) lost your place. Wire it to the platform's per-user
/api/music/now-playing (same endpoints the app uses):
- MusicPlayerHost persists a snapshot (track + position) on play/pause + track
change + a 10s heartbeat (position read via ref so the heartbeat stays live).
- On first load with an empty queue it restores that snapshot: rebuilds the
album queue (sortTracks), loads it PAUSED (browsers block autoplay on reload),
and seeks to the saved position once metadata is in. A restore guard stops the
load from clobbering the saved position with 0.
- MusicDetail auto-opens the currently-playing album once on mount (when it has
no location yet), so /music lands on the track — without yanking you back
after you navigate away.
Adds loadQueue() (paused) to useMusicPlayer + a NowPlaying type. tsgo clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The artist heart only rendered in the discography view (disco && !album), which
needs a _discography.md — so artists without one (plain grid of album folders)
had no heart. Add an artist header + heart to the grid view when on an artist
folder (crumbs>=2), matching the app's inArtist rule. favKey = artist music-rel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the web player to the platform's per-user /api/music/favorites (same
endpoints the app uses). New useMusicFavorites hook (react-query, shared
optimistic cache) + a reusable MusicHeart toggle, placed at:
- album header (album) + each track row (track, reveals on row hover, filled
favorites stay shown)
- album cards in the artist/grid views (album)
- artist discography header (artist)
- the now-playing player bar (current track)
Track key = homePath "Music/<rel>/<file>"; album/artist keys = music-rel.
No dedicated Favorites browsing view yet (hearts only), matching the app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
meta.json is written in ffprobe/readdir order (arbitrary), and the web player
rendered/queued it as-is — so albums like Andrew Bird's "The Mysterious
Production of Eggs" showed scrambled (11, 7, 5, 14, 1, …). The web Track type
didn't even carry the `track` tag.
Add `track` to the Track type + a shared sortTracks(): by track NUMBER (parsed
from the "n/total" tag), falling back to tag title only for tracks without a
number. Applied to the rendered tracklist (setAlbum) and both play queues.
Verified it reorders that album to 1→14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Panels are now container contexts (@container → container-type: inline-size)
on both the normal and transparent PanelSlot render paths, so panel content
can respond to the *panel's* width instead of only the viewport's. Non-
breaking: existing screens keep their viewport (lg:/md:) variants untouched.
First adopter: the bTop system-monitor grid reflows on panel drag —
grid-cols-1 → 2 at @min-[600px] → 3 at @min-[960px] (panel width), keeping
cards ≥300px so they no longer squeeze on a narrow panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full /api/system-monitor/* contract (stats snapshot incl. cpu/mem/disks/temp/
gpu/net/power, pm2, docker, and the two SSE log streams) with response shapes,
auth (Bearer or ?token=), owner-only note, and the net/power rate caveats — so
the app can implement the same views. Also removes the orphaned Pm2Logs.tsx
(superseded by LogStream) that a prior commit left tracked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three more same-level grid cards (bTop now 7 cards; Temperature stays under
Memory via natural 3-col flow):
- GPU: gpu_busy_percent + VRAM used/total from /sys/class/drm (instant).
- Network: ↓/↑ throughput (bytes/sec) from /proc/net/dev deltas between /stats
calls, aggregate + top interfaces.
- Power: CPU package watts via RAPL energy delta + GPU watts (amdgpu hwmon).
Note: RAPL energy_uj is root-only by default (Spectre-era lockdown), so CPU
package power shows "—" unless made readable (a udev rule); GPU watts work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend: readTemps() scans /sys/class/hwmon for all temp sensors and picks the
CPU one (k10temp/coretemp/zenpower Tctl/Tdie/Package); added to /stats.temp.
- BtopView: a Temperature card at lg:col-start-2 (under Memory, second row) —
big CPU °C (color-graded), its sensor label, and the other sensors (GPU, NVMe,
wifi…) listed beneath.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Shared LogStream component (solid-black <pre> terminal pane) replaces Pm2Logs;
pm2 and docker both drill into it.
- GET /api/system-monitor/docker/logs?id=<container>&lines=<n> — SSE of
`docker logs -f` (combined stdout+stderr); id charset-validated + spawn arg.
- DockerView: container cards are now clickable → live logs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GET /api/system-monitor/pm2/logs?id=<pm_id>&lines=<n> — SSE that spawns
`pm2 logs <id> --raw` (combined out+err, follows live) and streams each line.
id validated numeric + passed as a spawn arg (no shell); killed on disconnect.
- Pm2View: clicking a process name drills into Pm2Logs (EventSource tail with a
live pulse + back button); autoscrolls, capped at ~1200 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/system-monitor is now a WorkspaceView (like /music): a left ScopeList panel
selects the scope over the 'monitor:scope' channel, the right MonitorMain panel
renders it. Three scopes:
- bTop — the existing system snapshot (CPU/mem/disks/top processes)
- pm2 processes — new GET /api/system-monitor/pm2 (pm2 jlist → name/status/cpu/
mem/restarts/uptime table)
- dockers — new GET /api/system-monitor/docker (docker ps → container cards with
state/status/image/ports)
Both new endpoints degrade gracefully to an error field. Persisted as
screens/system-monitor; owner-only.
Needs a restart (backend routes + rebundle) + hard-refresh to appear.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the turn/session decoupling so nothing is lost across disconnects.
Phase 2 (durability):
- New chat_session_events table (global monotonic id = cursor) + queries
appendChatEvent / getChatEventsSince / pruneChatEventsOlderThan.
- Every durable outbound ServerMessage now goes through emitToSession: appended
to the queue (even while the client is disconnected) and delivered live with
its seq. Streaming deltas stay ephemeral (live-only, never persisted).
Phase 3 (resilient transport):
- New 'resume-cursor' client message → handleResumeCursor re-binds the socket to
the (still-live) session (cancels idle-GC via attachWs) and replays every event
since the client's cursor.
- useChatWebSocket already auto-reconnects; added an onOpen hook. useChat tracks
the max seq and, on every (re)connect with an established session, sends
resume-cursor — so a dropped connection self-heals with no manual navigate
away/back, and background task notifications that landed while offline replay.
Verified end-to-end: disconnect after a turn's result but before a background
task finishes, reconnect with the cursor → the missed task:notification is
replayed from Postgres, no duplicates.
Note: the DB is managed via drizzle push/direct DDL (no __drizzle_migrations
table), so 0001 was applied directly; the generated migration is committed for
the record.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root fix for orphaned background tasks: the platform drove Claude Code as a
one-shot `claude -p` per turn (stdin ignored, process exits at turn end), so
run_in_background/Monitor work — and its task_notification — had no live harness
to return to. Now each chat session runs ONE long-lived Agent SDK query() with
streaming input; turns are user messages pushed onto it, and the session stays
warm between turns.
- claude-manager: persistent `query({ prompt: AsyncIterable, options })` per
sessionKey (bypassPermissions, --resume, mcp via extraArgs, CLAUDECODE stripped).
Single consumer loop maps every SDK message → ChatEvent, incl. post-turn
task_started / task_notification. interrupt() = stop-turn; abort() = kill-session;
30-min idle GC.
- stream-parser: processMessage() (object-level, reused by the SDK loop) + task
message handling. ChatEvent/ServerMessage gain task:started / task:notification.
- API: the sidecar event subscription is now SESSION-scoped (no longer unsubscribes
on 'result'), so background events after turn-end still reach the client. First
turn opens the session; later turns push onto it. handleStop → interrupt (keeps
session warm); disconnect/deleteSession → kill.
- protocol/sidecar-registry/user-instance: claude:interrupt command + interruptClaude.
- client: render task:started / task:notification in the transcript.
Verified end-to-end through the real chat WS: a run_in_background task's completion
arrives ~6s AFTER the turn's result; multi-turn on one warm session works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a per-session teardown, distinct from the existing turn-only "stop":
- New WS 'disconnect' message → handleDisconnect → sessionManager.deleteSession,
which fires _claudeKill (kills any in-flight Claude/OpenCode turn) + _sidecarUnsub,
clears the idle timer, and drops the in-memory session. WS stays open so a new
prompt starts fresh. Server acks with 'disconnected'.
- useChat: disconnectSession() + a 'disconnected' handler (commit partial stream,
settle to idle).
- UI: an Unplug button in the chat DetailBar (shown while connected).
Scope note: targets the CURRENTLY-OPEN session (correct in-memory sessionKey).
Disconnecting an arbitrary *listed* session isn't wired yet — session-list rows are
keyed by the on-disk transcript uuid, which isn't the live sessionKey, so that needs
a reverse lookup + a REST endpoint. NOT yet deployed (needs a server restart).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MusicBrowser now lists a folder's children as single-column list items and drills
via the shared channel (libraries → artists → albums). When the current path is an
album leaf it lists the album's siblings and highlights it, so you can switch
albums from the left while the right shows the tracklist. Handles the non-uniform
library layouts via the manifest's tracks count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the screen into two registered panel apps that coordinate via a
'music:cwd' panel channel, like /chat:
- music-browser (left): library selector, publishes the path.
- music-detail (right): renders the path — album tracklist, artist discography
sections, or a folder grid — and drives the app-wide player.
MusicScreen is now a WorkspaceView over a horizontal 2-panel layout (persisted as
screens/music), so the panels are resizable. Registered in AppRegistry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Top-level dirs of ~/Music are libraries (tabs); within one you drill through
folders via /file-browser/ls with a breadcrumb until a folder has tracks, then
its songs (titled from the indexed /music/meta, filenames as fallback) with a
cover + play-all. Selecting a track feeds the app-wide player. Handles the
non-uniform library layouts (Albums/<Artist>/<Album> vs DJ Sets/<Artist>).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The widget browses the /api/music/* library (search albums → tracklist) and hands
a queue to an app-wide player. The player (useMusicPlayer, useGlobal-backed) and
its site-wide bottom dock (MusicPlayerHost) live in the persistent DashboardLayout,
so playback survives route changes. Dock has cover/title/artist, drag-scrubbing
(SeekBar), volume (persisted), and prev/play/next/close. The nav Dock slides up by
MUSIC_DOCK_HEIGHT while the music dock is present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A reload button (next to +) so newly-deployed widgets show up without a manual
hard-refresh after a rebuild+restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OCR now exists as a task, with recursion, multi-select scoping and a choice of
inline or job. The file viewer's OCR button is untouched, and /file-browser/ocr
stays — it is what both that button and the task call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extract Audio now exists as a task, with recursion, multi-select scoping, a
format choice and multi-track handling — none of which the one-shot menu entry
had, since it always produced a single mp3.
/extract-audio stays because the file viewer's button plays its output rather
than saving it beside the video, but it no longer returns a cached file: like
transcription and OCR, it always redoes the work. The path is now named outRel,
since it is an output location rather than a cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both endpoints kept a copy under cache/ and returned it on the next call, and
also short-circuited when the sibling .md already existed. So a re-run never
re-ran: a bad transcription stayed bad, and there was no way to ask for a fresh
one. Every caller passes saveNextTo, so the cache-path return was dead code
anyway.
Both now always do the work and overwrite the sibling. CACHE_PREFIXES drops the
two prefixes, since /save-result has nothing left to promote for them; the tts
and audio caches are untouched.
Also fixes the task runner output being unreadable in light mode. The panel is a
fixed dark terminal, but stdout lines were classed text-foreground, which follows
the app theme and renders black on the dark background. They now inherit the
pre's own colour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transcribe Audio covers it as a task, with recursion, multi-select scoping,
skip-if-already-done and a choice of inline or job — none of which the one-shot
menu entry had. Two ways to do the same thing from the same menu is worse than
one good one.
The file viewer's Transcribe button is untouched, and /file-browser/transcribe
stays: it is what both that button and the task call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes that Transcribe Audio needs.
Whisper's translate mode only ever outputs English, so it cannot honour
"translate into <language>" for anything else — it answered Portuguese audio with
a rough English rendering instead of a transcript. The translate decision is gone
and transcription is always faithful to the detected language; spokenLanguages
now only breaks ties on clips Whisper is unsure about.
Script tasks get OFFICER_API_URL / OFFICER_API_HOST / OFFICER_AUTH_TOKEN so they
can call Officer's own endpoints rather than reimplementing server-side work.
Requests go to 127.0.0.1 so nothing depends on DNS or the proxy, but origin
validation matches Host against PUBLIC_URL, hence the separate host variable.
`inline` accepts "ask", which offers both affordances in the runner — Run here
streams into the modal, Run as job queues it. Useful when the same task can take
a second or an hour depending on whether it was pointed at a file or a library.
Existing true/false values behave exactly as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CATEGORY_ORDER hardcoded Video/Audio/Cleanup in the frontend, so adding a
category meant a code change. The order now lives in categories.yaml at the root
of the items store and reaches the client via GET /tasks/categories — the
platform no longer knows any category by name.
The endpoint is declared before /:name, which would otherwise match
"categories". Categories used by a task but absent from the file still work: they
sort alphabetically after the listed ones, and Other stays last.
Menus consume grouped tasks rather than grouping them per row. Groups are built
from the tasks and the file only ranks them, so a category listed with no
matching tasks cannot produce an empty submenu — locked in by tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every task declares a directory trigger, so right-clicking any folder listed all
fifteen at once — Tag Album offered on a folder of photos. TASK.md gains an
optional `category`, and the context menu nests by it: Run Task > Video > …
Nesting only kicks in when more than one category matches. A .mp4 matches eight
tasks that are all Video, so file menus stay flat rather than gaining a pointless
hop. Known categories lead (Video, Audio, Cleanup); anything else follows
alphabetically with Other last.
Applied to both menus — the right-click one and the ⋮ dropdown — which carried
identical blocks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>