Skip to content

CI: /compact (self-check)#2

Open
lemotw wants to merge 107 commits into
mainfrom
feat/compact-button
Open

CI: /compact (self-check)#2
lemotw wants to merge 107 commits into
mainfrom
feat/compact-button

Conversation

@lemotw

@lemotw lemotw commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Self-check PR to run the fork's CI (test + E2E) on feat/compact-button.

Not for merge — the real PR is ygncode#74. This only exercises CI on the fork.

setkyar added 18 commits June 8, 2026 01:25
)

* refactor: replace session modal window bridges with a reactive store

Introduce web/src/session/session-modals.svelte.js, a $state-backed store
that owns the session viewer's modal open-state (shortcuts, model-usage,
fork, cat-settings, label). SessionPage binds the modal components to it and
every opener — the command menu, keyboard globals, content runtime, and
cat-gatekeeper — imports the store helpers instead of reaching through
window.__piOpen*.

This removes the five global openers and the onMount ordering hazard they
created. The store is live-only and never pulled into the static export
(TestExportBundleIsSelfContained stays green). Phase 5 step 1.

* refactor: replace session component-handle window bridges with a registry

Add web/src/session/session-runtime.js, a plain singleton registry whose
slots (annotations, artifacts, rightSidebar, layout) hold the imperative
control surfaces child components used to publish on window.__pi*. Each
component assigns its slot on mount and clears it on destroy; consumers
(SessionTree, LiveReload, session-ui-runner, session-globals, SessionPage)
read sessionRuntime.<slot>?.method() instead of reaching through window.

__piCatGatekeeper was a window round-trip inside CatGatekeeper.svelte; it now
uses a hoisted local controller. SessionPage resets the registry on unmount.

The registry is dependency-free and stays out of live-only code, so the
export bundle (rebuilt) no longer references any of these globals and
TestExportBundleIsSelfContained stays green. Phase 5 step 2.

* refactor: move toggle-state handle into the session runtime registry

Replace the last window globals in the session UI layer with the
sessionRuntime registry. setupSessionUi now registers the toggle controller
as sessionRuntime.toggleState instead of writing window.sessionToggleState +
window.applyToggleStateToNode; the message-pane afterRender hook (live
content-runtime and the static export) reads sessionRuntime.toggleState
?.applyToNode(). Drops the dead window.sessionToggleState (no readers) and
export's __piFilterState (written, never read).

The imperative setupSessionUi runner itself stays: it is shared by the live
app and the server-less export to wire the static session.html DOM, so it is
legitimate shared infrastructure, not migration residue. Folding that wiring
into components would require migrating export's static sidebar chrome to a
component mount (a separate, E2E-gated change). Phase 5 step 3.

* feat: client-side SPA navigation via pushState/popstate

Make App.svelte's router reactive: it now listens for popstate (back/
forward) and wraps history.pushState/replaceState to swap the active
page when the pathname changes, instead of reading the path once at
mount. Route the index/settings navigation through a shared navigate()
helper so links no longer trigger full page reloads: session cards, the
command palette, new-session creation, the settings menu link, the Cmd+,
shortcut, and the settings back link. External target=_blank links and
modified clicks (Cmd/Ctrl/Shift/Alt, middle-click) keep their default
browser behaviour.

* feat: client-side session→session navigation

Key <SessionPage> on the ?id= query param so navigating between sessions
(same pathname, different id) tears down and remounts it instead of
falling through as a no-op — SessionPage reads ?id= only at mount.
App.svelte now tracks search alongside pathname to drive the key. Route
fork/clone/new-session navigation (CommandMenu, SessionHeader, the entry
fork in session-content-runtime) through navigate() so they no longer
trigger full page reloads. Within-session navigation never mutates the
URL, so the key stays stable while reading a session.

Also remove the pi-session-reload listener in SessionPage's onMount
cleanup, which a remount would otherwise leak.

* fix(e2e): restore cat-gatekeeper test hook and pin its sleep window

The dd7d654 registry refactor dropped window.__piCatGatekeeper, which the
cat e2e test uses to force a break (skipToBreak) without waiting out the
25-min focus timer. The controller has no in-app consumers, so re-expose
it as a plain window seam set on mount / cleared on destroy rather than a
sessionRuntime registry slot.

Also pin the test's bedtime == wakeup (zero-width sleep window): the
default 23:00–07:00 window made the gatekeeper enter bedtime mode when the
suite ran overnight, showing the sleep overlay instead of the break and
failing across all browser projects.

* test(e2e): de-flake load-earlier by dropping the intermediate poll

The per-window expect.poll (banner gone OR button re-enabled) was the
flake source: prepending the merged conversation triggers a synchronous
full re-render that, under the parallel matrix, briefly jams the main
thread and can blow the fixed 15s poll budget even though the load
succeeds (the failure snapshot showed the banner already gone and the
earliest message rendered). Drive the clicks with click()'s built-in
enabled-auto-wait plus a best-effort settle, and let only the final
auto-retrying assertions (banner count 0 + earliest message rendered) be
authoritative, so a transient render jam no longer fails the test.

* fix: blank settings page when navigating with a custom font set

AppearanceSettings.fontSelectValue() wrote $state (customVisible,
customValues) while computing the font <select> value inside the {#each}
font-kinds block. An {#each} is a Svelte block effect, so that write
throws state_unsafe_mutation — but only when the component mounts during
a client-side route swap (the new pushState navigation) rather than a
fresh page load, and only when a custom (non-builtin) font is configured
so the branch runs. The error aborted the swap, leaving the settings
page blank (or the source page stuck).

Make the template-called helpers pure: derive the select value and the
custom-field visibility from settings, and track in-progress edits in a
draft written only from event handlers. Add an e2e regression
(client-side nav to /settings with a custom font) that blanks without
this fix.

* test: fix data race in fakeSender

handleNewSession initializes the worker on a background goroutine, so the
chatSender's methods (EnsureWorker/SetModel/GetState/...) run concurrently
with test assertions that read the fake's recorded fields — a data race
under -race (e.g. TestHandleNewSessionCopiesSourceModelAndThinking polling
ensureWorkerSessionID).

Guard the mutated fields with a sync.Mutex: lock the writes in each sender
method (channel sends stay outside the lock) and read them in tests through
accessor helpers. Config fields set once at construction stay lock-free.
Test-only; no production behavior changes.
* refactor(session): extract live runtime helpers

* docs(dev): record session refactor goals
@lemotw lemotw force-pushed the feat/compact-button branch 2 times, most recently from 2d78edf to 6a0ee52 Compare June 8, 2026 05:46
setkyar added 27 commits June 9, 2026 00:28
Replace the five void-dependency reads in the auto-scroll $effect with a
single $derived transcriptSignature, removing the Svelte 5 anti-pattern
where dropping a dep silently breaks tracking.
Introduce session-title.svelte.js as the single source of truth for the
session name. SessionHeader renders it and owns the document.title sync;
CommandMenu's rename and LiveReload's auto-title now update the store
instead of reading/mutating #session-header-title in foreign DOM.
Add shared/links.js (repo, user-docs, Telegram) and consume it from the
command menu, home menu, and About settings instead of repeating the
literals. Name the command-menu close-animation timeouts (260/160ms)
that mirror the CSS transition durations.
Extract newSessionSummary and applySummaryContext so ParseSummary and
ParseFile share the summary init, LastActivity fallback, name precedence,
and cwd/chat-disabled handling (incl. the disabled-reason string) instead
of duplicating them. The per-line scan stays typed vs map to preserve the
index fast path.
Extract the rename/fork/clone/load network calls into session-menu-actions.js
(with unit tests), and drive the desktop popover and mobile panel from shared
primaryItems/footerItems descriptors via a snippet instead of duplicating the
item markup. data-action attributes and classes are unchanged, so the existing
click dispatch keeps working.
Closes 3 Dependabot alerts (GHSA-hcg3-q754-cr77, GHSA-j5w8-q4qc-rx2x,
GHSA-f6x5-jh6r-wrfv) covering SSH key-exchange DoS, ssh unbounded memory,
and ssh/agent OOB-read panic.
Closes Dependabot alert GHSA-mh63-6h87-95cp (excessive memory allocation
during JWT header parsing).
The rpc copy was never referenced; the canonical declaration lives in
internal/workers/manager.go.
ChatAvailable and ChatDisabledReason are coupled, so the OR clause always
reduced to ChatAvailable; use it directly.
Use the exported DefaultIdleTTL directly in NewManager.
The {#if modalOpen} block rendered nothing; modals manage their own
overlays. Drop the block and the now-unused derived.
The navigator.clipboard -> execCommand fallback was reimplemented in 5
places. Centralize it in web/src/shared/clipboard.js (copyToClipboard,
returning success) and have the copy sites delegate, keeping their own
flash/toast UI. Update the export guard tests to assert against the shared
helper.
Export MOBILE_BREAKPOINT_PX/MOBILE_MEDIA_QUERY from session/ui/sidebar.js
and route every (max-width: 900px) JS check through isMobileLayout(),
including CommandMenu which previously reimplemented it inline.
handleApiForkSession/handleApiCloneSession launched
initializeNewSessionWorker in a goroutine bound to the request context, so
a client disconnect could cancel worker creation mid-flight and leave a
half-created worker. Match handleNewSession and use context.Background().
server.New() always sets s.now, so call it directly: remove projects.go's
nowTime() wrapper and chat.go's inline guard. Tests that built Server
literals relied on the guard, so set now: time.Now in those, matching how
production constructs the server.
AGENTS.md bans unicode glyphs for icons. Swap the annotation-delete and
btw-close × for the X icon, and the btw send/stop ▷/◼ for Send/Square.
Adds Square to the shared icon set.
Add share.* keys to en.js and replace the raw English title, field
labels, and action buttons in ShareDialog with t() lookups (reusing
menu.share and common.close).
The back-button and close-button aria-labels were hardcoded English. Use
t('common.close') and a new common.closeNamed for the titled variant.
Declare settingAutoTitleEnabled/Mode/Model constants and use them in
auto_title.go and the settingDefaults map, matching the named-key
convention of the other settings.
Decode the common type/level fields once into a typed struct and fold the
stray if-blocks into the existing switch, instead of double-unmarshalling
each line and re-checking raw[type] in four places.
… (B15)

The function called time.Now() 3-4 times, so the filename and header (and
the model/thinking entries) could straddle a clock tick. Capture now once
and reuse it for the filename and every RFC3339 timestamp.
Replace the os.ReadFile + strings.Split full-read (which allocated the
content twice and lacked a line-size guard) with the same bufio.Scanner +
bounded-buffer pattern used by the other parsers.
…s (B10)

Pull the four-map auto-title bookkeeping and the metrics-dashboard fields
out of the Server god struct into named metricsState/autoTitleState
sub-structs, so each subsystem owns its fields and lock and ownership is
documented at the type level.
Replace scattered inline durations with named consts: annotation flash
(1200), artifact "Copied" reset (1500), btw idle grace (3000) / status
poll (1500), sessions reload debounce (500), and right-sidebar reset width
(320).
…(F16)

artifacts/hiddenCount/selectedId are now $derived from `collected` (live)
or an imperative $state (standalone), so the self-looping
untrack(setArtifacts) $effect is gone. Selection clamps to the list via a
derived; preview resets on selection change.
Drive the active-tab via $state with template class:/aria-selected/hidden
bindings instead of the createRightSidebarTabs classList controller, and
inline the collapse/expand <body>-class logic with template-wired buttons,
removing the two controller modules. Visibility stays synchronous (it
shifts page layout and callers expect an immediate effect). Resize,
scratchpad, and help-modal wiring remain imperative.
AnnotationLayer now declares its config (api, scopes, composerEl, countEl,
callbacks, selectionDelayMs) as $props() and sets up via a reactive $effect
once the api + scopes are present, dropping the imperative init() up-call
and its silent no-op-if-never-called failure mode. SessionShell assembles
the config — DOM anchors resolved after mount, callbacks routed through the
shared session runtime — and threads it through RightSidebar, replacing the
session-page-annotations.js wiring (deleted). Tests pass config as props.
@lemotw lemotw force-pushed the feat/compact-button branch from 6a0ee52 to 4699154 Compare June 9, 2026 11:16
Adds a "Compact context" button to the context-usage popover plus a
composer-scoped Cmd/Ctrl+Shift+K shortcut. Both call a new POST /api/compact
endpoint that runs pi's dedicated `compact` rpc command (session.compact()).

Compaction is NOT a chat prompt: pi's rpc prompt path only expands
extension/skill/template commands, so sending "/compact" as a message would
reach the model as literal text and never compact. The backend adds
BuildCompactCommand, piRPCWorker.Compact, Manager.Compact and handleCompact
(async; broadcasts a reload once compaction completes).

UX:
- "Compacting…" banner above the composer + a status label pinned to
  "compacting" while it runs (guarded against firing while the worker is busy).
- updateContextUsage is now compaction-aware: it mirrors pi's
  estimateContextTokens so the context ring reflects the reduced size after a
  compaction entry (summary + kept entries, chars/4) instead of stale
  pre-compaction usage, switching to the measured value on the next turn.

Tests: vitest (compact endpoint path, banner show/hide, context estimate vs
measured) and Playwright (real compaction marker, banner, no prompt echo) with
a stub pi that implements the compact command.

Closes ygncode#41
@lemotw lemotw force-pushed the feat/compact-button branch from 4699154 to 3eb330f Compare June 9, 2026 11:24
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.

2 participants