Skip to content

feat: mobile Sessions sheet + touch actions (stacked on session sidebar) - #2

Merged
Direct-Launch merged 9 commits into
feat/session-sidebarfrom
feat/session-sidebar-mobile
Oct 2, 2026
Merged

Direct-Launch merged 9 commits into
feat/session-sidebarfrom
feat/session-sidebar-mobile

Conversation

@Direct-Launch

Copy link
Copy Markdown
Owner

Stacked on #1 (chat session sidebar). Base is feat/session-sidebar; will retarget to main after #1 merges.

What

Makes the session list usable on mobile / narrow panes, where the in-leaf sidebar is hidden (<600px).

  • Extracted a shared SessionList.svelte — the in-leaf sidebar and the mobile sheet render the exact same list (no divergence).
  • Touch overflow "⋯" menu on each row (Obsidian Menu: Open/Pin/Archive/Rename/Delete/Restore). Hover action buttons are kept on pointer devices via @media (hover), so desktop is unchanged and touch is fully served.
  • SessionSheetModal (extends SvelteModal) hosts the shared list; full-screen on phones. Opening a chat closes it.
  • "Sessions" trigger in the chat view, shown only when the in-leaf sidebar isn't (!wide && enableSessionSidebar).
  • Active-row highlight is driven by the reactive ThreadPathStore, so it stays correct in both the sidebar and the sheet.

Gates

npm run check clean · biome lint 0 errors · feature tests pass (vitest) · npm run build succeeds. Each step reviewed; whole-branch review clean.

Note

Not yet smoke-tested on a real mobile device — desktop verifies the trigger→modal→open/close flow (narrow the pane <600px); the touch "⋯" menu shows on touch devices. Recommend a phone check before merge.

🤖 Generated with Claude Code

@Direct-Launch

Copy link
Copy Markdown
Owner Author

Same battery, run against this branch.

  1. Gates (Clean): biome lint 0 errors (502 files), npm run check 0 errors / 0 warnings, npm test 144 files / 2002 tests passing, npm run build succeeds — main.js 6,328.76 kB against 6,326.81 kB on feat(sessions): chat session sidebar, run cues and custom sound files #1, so the mobile work adds roughly 2 kB.

  2. Coverage (Unchanged): this branch adds no new test files, so the count is identical to feat(sessions): chat session sidebar, run cues and custom sound files #1. The shared SessionList extraction is exercised by the tests feat(sessions): chat session sidebar, run cues and custom sound files #1 introduced rather than by new ones — worth knowing if you expected the refactor to carry its own.

  3. Resize persistence (Carried, not introduced): the per-pointermove settings write from feat(sessions): chat session sidebar, run cues and custom sound files #1 is unchanged here, as these commits do not touch DataStore. Same recommendation — settle it before a live smoke-test.

Nothing here regresses the base. Same caveat as #1: gates measured, no live-vault smoke-test.

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.
@Direct-Launch

Copy link
Copy Markdown
Owner Author

Resize fix integrated with pointer capture — d5280f1

Pushed on top of 886d30b. The resize work now separates rendering from persistence and holds the pointer for the duration of the gesture:

  • previewWidth on every pointermove, a single commitWidth on pointerup — the model from fcae8b5, unchanged.
  • New in d5280f1: setPointerCapture() on the divider, released in onUp(), plus a pointercancel listener. Without capture, a release that lands outside the 5px strip — or over an iframe/webview — is lost, and the drag stays stuck to the cursor after the button is released.
  • Both capture calls are guarded, so an environment without pointer-capture support still delivers the gesture through the window listeners.

Gates on this head (d5280f1)

Gate Result
biome lint src test exit 0
svelte-check 0 errors, 0 warnings
vitest run 146 files / 2011 tests, 9 of them new (resize + status)
npm run build build/prod/main.js 6,336.36 kB, exit 0

The single test failure is test/skills/bootstrapDefaultSkills.test.ts → leaves an up-to-date skill untouched despite CRLF and a trailing newline. It reproduces identically on the untouched base commit (886d30b), so it is not caused by this change — it is sensitive to a Windows checkout with core.autocrlf=true.

Still outstanding

This has not been smoke-tested against a live Obsidian vault. The remaining gate is a real drag on the divider (and the save-count check it is meant to satisfy), which needs a running app.

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.
@Direct-Launch

Copy link
Copy Markdown
Owner Author

Run cues (sound + notification) — cf5f48a

Adds a cue when a chat run settles, as one call site rather than a hook per outcome.

Where it hooks. ChatSession's finally already receives a fully normalised outcome: the catch above it maps cancelled → cancelled, a post-success bookkeeping failure → stays success, and everything else → error. Reading the settled assistant state there gives exactly one cue per run, fired from the session so it happens once no matter how many panes have the thread open.

Behaviour

  • success → short positive tone · error → a distinctly lower tone · cancelled → silent (the user asked for the stop, so a cue is noise, not information) · non-terminal states fire nothing.
  • Sound and notification are independent settings, both defaulting on.
  • Audio is unlocked on the send path: a run always starts from a user gesture, which is the only moment Chromium lets an AudioContext open so it can be heard later.
  • Notification feature-detects and falls back to Notice, which is the guaranteed floor and is not permission-gated.
  • Every emission path is guarded — a blocked cue can never throw into a run that has already settled.

One defect found and fixed during this work, worth recording

The first cut read the settings inline as arguments to emitRunCue(...). The read throws when no plugin is initialised, and because arguments evaluate before the call, the guard inside emitRunCue never ran — so the cue brought down the bookkeeping of the finally itself. chatSessionPostRunFailure caught it: 5 failures (Plugin does not exist, chatStore.svelte.ts:778). Settings are now passed as a thunk and read inside the guard, which is precisely the failure mode the task warned about: it must degrade silently, not throw.

Gates on cf5f48a

Gate Result
biome lint src test exit 0
svelte-check (both configs) 0 errors, 0 warnings
vitest run 147 files / 2017 tests — 6 new in test/stores/runCue.test.ts, all passing
npm run build build/prod/main.js 6,338.37 kB, exit 0

The single failure remains test/skills/bootstrapDefaultSkills.test.ts (CRLF), which reproduces on the untouched base — pre-existing and unrelated, as noted earlier.

Still outstanding

Not smoke-tested against a live Obsidian. The audio and notification halves are feature-detected and cannot be exercised in this repo's test environment; the decision logic is unit-tested, the emission is not. A live check needs a real run started from a gesture.

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