Skip to content

feat(sessions): chat session sidebar, run cues and custom sound files - #1

Merged
Direct-Launch merged 15 commits into
mainfrom
feat/session-sidebar
Oct 2, 2026
Merged

Direct-Launch merged 15 commits into
mainfrom
feat/session-sidebar

Conversation

@Direct-Launch

@Direct-Launch Direct-Launch commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

What

The whole session-sidebar stack in one PR. Consolidates what were three stacked PRs — #1 (desktop sidebar), #2 (mobile sheet + touch actions) and #4 (custom sound files). #2 and #4 are closed; every commit is in this branch, so the diff is the sum of the three.

  • Desktop session sidebar — the chat list, with a drag-resizable width that persists.
  • Mobile Sessions sheet with a touch overflow menu.
  • Per-row run-status indicator (✓ / ⚠ / ⛔) that survives navigation and session eviction — a thread that settled while unloaded reads its status back from persisted thread flags.
  • Run-settle cue — a tone and a Windows notification when a run settles; cancelled runs stay silent, and the notification is titled with the chat’s name.
  • Optional custom sound files — runSuccessSoundPath / runFailureSoundPath take absolute OS paths, decoded at play time, falling back to the built-in 880/320 Hz tone when missing or undecodable.

How I tested it

Real vault, Obsidian 1.13.7, desktop (Windows):

  • Sidebar drags and releases cleanly; the width survives a reload; per-row status is correct.
  • A backgrounded successful run fires the cue, and the notification title names the chat.
  • Moving across three or more chats keeps each row’s status icon.
  • With a success sound path set, the configured file plays when a run settles — not the built-in tone.

Not tested: the mobile sheet on a real device. Everything above is desktop.

AI assistance: drafted by an AI agent (Smart Second Brain); reviewed and tested live by me.

Checklist

  • check / format / lint pass locally — via the npm equivalents (npx biome format, npx biome lint exit 0 across ./src ./test ./integration; svelte-check 0/0 on both configs). bun is not available on the machine this was prepared on.
  • test — one failure, test/skills/shippedSkillHistory.test.ts, pre-existing and Windows-clone-only (core.autocrlf=true, no .gitattributes); it fails identically with these changes reverted and CI checks out LF.
  • Tried the change in a real Obsidian vault (above)
  • I read CONTRIBUTING.md
  • No provider, bundled skill or built-in tool; manifest.json is unchanged

Notes

CONTRIBUTING asks for PRs against main — this one is, and the earlier stacked bases are gone with the closed PRs.

…pstream 2.2.0

In-leaf resizable session list: Pinned/Archived sections, search, sort,
new/open/rename/delete, pin/archive (running-guard), resize + width persist,
reveals >=600px. Plugin-data-backed flags with rename/delete lifecycle.
Rebased onto current upstream (was 2.1.0); resolved dataStore import +
Chat.svelte layout conflicts (dropped upstream-removed messageNavHotkeys).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Direct-Launch

Copy link
Copy Markdown
Owner Author

Re-ran the full gate battery against this branch. Baseline is the PR's own base (main @ f220df2), so the comparison is like-for-like.

  1. Gates (Clean): biome lint 0 errors (500 files), npm run check 0 errors / 0 warnings, npm test 144 files / 2002 tests passing, npm run build succeeds — main.js 6,315.04 kB on base to 6,326.81 kB here. The five new test files are the delta and nothing regresses.

  2. Test-suite claim (Corrected): the body says "Pre-existing unrelated bootstrapDefaultSkills failures are not touched by this branch." I could not reproduce any pre-existing failures — base main is 1981/1981, exit 0. Worth dropping that line, as a reviewer will read it as their own main being red.

  3. Resize persistence (Blocks the incident fix): the divider persists settings on every pointermove, with no debounce, no coalescing and no commit on pointerup — resize.ts onMove calls setWidth per move, which reaches void this.saveSettings() and a full saveData of the settings object. That is the write pattern that corrupted data.json in a live vault. Committing the width once on pointerup (or debouncing it) would close it off before anyone drags this divider with real connectors configured.

One note for whoever builds the test vault, unrelated to this branch: vite.config.ts sends production builds to build/prod, but setup-vault and integration/README.md both symlink build/smart-second-brain — so the documented build-then-setup-vault sequence links an empty directory.

Not smoke-tested in a live vault — the gates are the evidence, and the resize point is a code-level finding.

Best regards,
Eusef

🤖 Written with Claude Code

…rmove

The divider assigned data.sessionSidebarWidth on every pointermove, and the
store setter saves on assignment, so a single drag produced a burst of
whole-file data.json writes - the write pattern behind the 2026-09-27
corruption incident.

Rendering and persistence are now separate: a drag previews into
component-local state and commits once on pointerup; a held arrow key is
coalesced through a 250ms quiet period; destroy() flushes so a chosen width
is not lost on unmount.

Covered by test/components/sessionResizePersistence.test.ts - a gesture
persists exactly once, and the value still survives a reload.
Each row now shows what happened to its last turn instead of only a
spinner for running sessions. Running, done, failed, cancelled and
interrupted each carry a distinct icon AND a text label, so colour is
never the only signal.

idle is the honest unknown, not a synonym for done: the registry only
holds sessions that have been opened, so an unopened row has no live
state to read and renders nothing rather than implying an outcome. A pair
left streaming by a mid-run close reports interrupted - it is neither
done nor failed, and guessing would put a false claim on a status
surface.

Terminal state comes from the last message pair's assistant state, which
the session already holds reactively, so a row updates as a run settles.
…rip ends the drag

Integrates the two parallel resize fixes: the commit-on-pointerup width model with its four tests, plus pointer capture and pointercancel handling. Without capture a pointerup landing outside the 5px strip, or over an iframe, is lost and the drag sticks to the cursor.
One cue per settled run, fired from ChatSession's finally so it happens once however many panes have the thread open. The outcome is read from the state the catch block has already normalised, so a post-success bookkeeping failure still reports success rather than a false failure. Cancelled runs stay silent. Sound and notification are independent toggles, both defaulting on. The settings read is guarded: the finally can execute with no plugin initialised (headless, or the unit tests for this path) and a cue must never break the bookkeeping of a run that has already settled.
findPairAcrossRebuild prefers stableKey, but sendMessage builds the optimistic pair without one, so after the settle rebuild the lookup matched nothing and a successful run emitted no cue. Read the settled state from the pair the run holds and treat the lookup as a preference. A regression test drives the rebuild seam, and both new tests go red without this fix.
… status icon on navigation

Two follow-ups reported after the live smoke test (2026-10-01).

1. The Windows notification said "Chat run finished" without saying which chat.
   The settle cue now carries the session name and uses it as the notification
   title. Notice, the permission-less floor, composes the name in. The name is
   mirrored onto the thread's session flags from the loaded thread title, so a
   backgrounded settle can name a chat that is not open.

2. The per-row status icon vanished when navigating between sessions. A live
   session knows a row's outcome, but the registry keeps only MAX_PARKED_SESSIONS
   idle sessions and evicts the rest, so the in-memory state is gone and statusFor
   fell back to idle. The settle handler now persists the outcome on the thread's
   flags and rows read it back via deriveRowStatus. Live state wins when present,
   the persisted outcome is the fallback, and idle keeps its honest meaning.

Tests: the new assertions were shown red with the fix reverted and green with it
restored. svelte-check 0/0. Full suite 147/148 files, 2028/2029 pass, the one
failure being the pre-existing bootstrapDefaultSkills CRLF test, untouched here.
Production build exit 0.
A settlement cue could only be the built-in 880/320 Hz tone. Users who
want their own sound had no way to supply one.

Add optional success/failure sound-file paths, read from disk at play
time and falling back to the built-in tone when the file is missing or
undecodable. Desktop-only: the fields are hidden on mobile, and the
reader returns null where there is no filesystem.

The decision stays pure (cueForOutcome) so it is testable without an
AudioContext; only emission touches the filesystem. Decoded buffers and
in-flight decodes are cached per path so a settle does not re-read the
same file.
@Direct-Launch Direct-Launch changed the title feat: chat session sidebar (Claudian-style) feat(sessions): chat session sidebar, run cues and custom sound files Oct 2, 2026
CI caught eight files the formatter would have rewritten — the local check had
been run with the wrong binary (npx resolved `biome@0.3.3`, not the repo's
`@biomejs/biome@1.9.4`), so it reported a false pass.

Formatting only; no behaviour change.
@Direct-Launch
Direct-Launch merged commit 06b812c into main Oct 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant