| Age | Commit message (Collapse) | Author |
|
|
|
|
|
chats)
|
|
Vision & vision-handoff frontend (consumes the backend's additive
[email protected] / [email protected] image types — no version bump).
Contracts mirrored:
- .dispatch/wire.reference.md: ImageChunk added to the Chunk union +
ImageChunk/ImageInput interfaces.
- .dispatch/transport-contract.reference.md: ChatRequest.images,
ModelMetadata.vision, + ImageChunk/ImageInput re-exports.
Core (core/chunks):
- conformance: assertChunkExhaustive handles the new 'image' variant
(the guard caught it — its purpose).
- appendUserMessage(state, text, images?) echoes a [text, image, ...]
user run; the user-message event dedup scans the trailing user run
(not just the last chunk) so an image-bearing echo doesn't duplicate
the text; applyHistory's during-gen dedup matches a multi-chunk echo
by content equality (chunkContentEquals + trailingRun helpers).
UI:
- ChatView renders user 'image' chunks as lazy <img> bubbles; a
non-vision model's persisted [image, analysis-text] both render.
read_image tool renders generically (no special-casing).
- Composer: clipboard paste / file picker / drag-drop of images ->
base64 data URLs, thumbnail previews with remove, forwarded on
chat.send (omitted when none). Image-only sends allowed; steering
(chat.queue) never forwards images.
- ModelSelector: vision badge (isVisionModel) marks vision-capable
models; indicator shows native-vision vs vision-handoff hint.
Store wiring: ChatStore.send + AppStore.send + App.svelte handleSend
thread images through; chat.send still omits cwd (only images added).
Verification: svelte-check 0/0; vitest 901/901 (run twice, +34 new);
biome clean; vite build OK. See backend-handoff.md §2j.
Not merged or pushed.
|
|
- backend-handoff.md: full §2j slice (API surface, contract note, FE summary,
verification, the single-provider GET note, worktree symlink note) + bump the
pinned transport-contract version 0.22.0 → 0.23.0 in the header/packages/
mirror/endpoints sections.
- ConcurrencyView.svelte: remove an unused ConcurrencyLimitView type import
(the type flows via viewConcurrencyLimits' return type; svelte-check clean).
|
|
|
|
|
|
# Conflicts:
# src/app/App.svelte
# src/app/store.svelte.ts
# src/app/store.test.ts
# src/features/workspaces/ui/WorkspaceCard.test.ts
|
|
|
|
backend, dispatch-web → frontend)
|
|
endpoint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sidebar wiring
|
|
# Conflicts:
# src/features/workspaces/ui/WorkspaceCard.svelte
# src/features/workspaces/ui/WorkspaceCard.test.ts
# src/features/workspaces/ui/WorkspacesHome.svelte
|
|
When the user clicks Test, the button now transitions spinner → ✓ (green) on
success / ✗ (red) on failure (persisting until the next test or computer
change), instead of reverting to plain 'Test' text with no success indicator.
Also refresh the connection-status badge immediately after a test completes, so
a stale 'connecting' spinner caught by the 4s poll mid-test is cleared and the
badge reflects the real post-test state. Clear the test result when the selected
computer changes so a stale ✓ from alias A doesn't persist for alias B.
|
|
de022ce)
Backend merged dev into feature/ssh-support (de022ce): TurnProviderRetryEvent
is now present alongside the SSH types. FE re-synced both file: deps; typecheck
is GREEN (0/0) with ZERO FE code changes (the consumer was already complete).
Full suite: 795/795 tests, biome clean, build OK. §2e verified post-merge.
|
|
Mirrors the cwd/workspace UI for the SSH-computer feature:
- New feature library src/features/computer/:
- logic/view-model.ts (pure): viewComputer/viewComputerStatus/viewTestResult/
summarizeComputers/formatHost/knownHostLabel + state->badge for the 4
ComputerStatusResponse states + SaveComputer/LoadComputerStatus/
TestComputer/LoadComputers ports. 20 view-model tests.
- ui/ComputerField.svelte: per-conversation selector (dropdown +
connection-status badge + Test-connection, polling the selected alias).
- ui/ComputerSelect.svelte: reusable Local/computers dropdown, shared with
the workspace default-computer control.
- AppStore: computerId state + refreshComputer (at every focus site, parallel
to refreshCwd) + setComputer (PUT /conversations/:id/computer, null=clear) +
global computers catalog (GET /computers on boot, like models) +
computerStatus(alias) + testComputer(alias). chat.send UNCHANGED (resolved
server-side like cwd).
- App.svelte: ComputerField in the Model sidebar view next to CwdField; adapted
ports wrap the store.
- workspaces: setDefaultComputer on WorkspaceHttp+WorkspaceStore
(PUT /workspaces/:id/default-computer); default-computer selector in
WorkspaceCard (reuses ComputerSelect); router passes store.computers through.
- Re-mirrored .dispatch/transport-contract.reference.md (Computers section +
ChatRequest.computerId); updated .dispatch/wire.reference.md (Computer/
ComputerEntry/defaultComputerId + the provider-retry divergence note from
handoff #1); GLOSSARY + backend-handoff.md (handoff #2, §2e).
Transparency invariant: the computer is USER-facing only (a tool-execution
target, never part of the model prompt); the agent never sees it.
Verify: 795/795 tests green; biome clean; vite build succeeds; 0 typecheck
errors from the computer feature. (11 pre-existing svelte-check errors remain
from the open §2d provider-retry divergence — backend feature/ssh-support
still lacks TurnProviderRetryEvent; not from this feature.)
|
|
The 'Open' button on a workspace card did client-side navigation
(onNavigate -> pushState), opening the workspace in the SAME tab.
Replace the JS click handler with a native anchor using target="_blank"
(+ rel=noopener noreferrer) so clicking Open opens the workspace in a
NEW browser tab. The new tab does a full load and routes to /<id> via
the existing parsePath -> App wiring.
The card no longer needs the onNavigate prop (its only navigation was
the Open button), so drop it from WorkspaceCard + WorkspacesHome and
update the card tests to assert the new-tab link attributes instead of
the onNavigate call.
|
|
The conversation tab's title span was truncated with a static
'Double-click to rename' tooltip. Replace the static title with the
full tab.title so hovering reveals the complete conversation title
(the truncate class still clips the visible label).
|
|
features + app wiring
- workspaces: URL-driven conversation grouping (home listing at /, routing,
store, http adapter, WorkspaceCard) wired into the App.svelte shell
- rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature)
- new features: mcp (status view), settings (chat-limit field), system-prompt
(prompt builder), all rendered via the generic surface host
- chat: store + ChatView updates
- tabs: tabs-store updates
- app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte
This commit makes HEAD typecheck clean for the first time: the prior HEAD
(c95cc77) imported features/settings from app/App.svelte but never committed
the feature, so only the full working tree was green.
|
|
- core/chunks: add retry-banner view-model (+test), update reducer/selectors/types
- core/wire: update conformance checks (+test)
- adapters/ws: update reconnect logic (+tests)
- adapters/history: new client-side routing adapter (history + popstate wrapper)
|
|
- Regenerate .dispatch/{wire,transport-contract}.reference.md mirrors
- Bump deps (package.json, bun.lock)
- Update AGENTS.md, GLOSSARY.md, README.md, ROADMAP.md
- Add workspaces backend-handoff exchange + notes/assumptions-log.md
- Add scripts/fix-dist-perms.sh (root-owned dist/ from Docker build)
- Add scripts/live-probe-provider-retry.ts
|
|
When a draft is promoted to a real conversation, send() appends the
user message as a provisional chunk (optimistic echo). But load()
also fires syncTail, which fetches the CR-6 persisted user message
as a committed chunk — showing the message twice until turn seal.
Fix: applyHistory now removes provisional chunks that duplicate the
last committed chunk (matching role + text content) when new committed
chunks arrive during generation. The optimistic echo is dropped once
the authoritative committed version arrives.
684 tests green.
|
|
Double-click a tab's title to enter inline edit mode. Enter or click
away (blur) saves; Escape cancels. The rename is optimistic — the
local tab updates immediately and PUT /title fires in the background.
683 tests green.
|
|
Replace dracula with a custom monokai theme (from dachinat/daisyui-themes,
with modifications: all -content colors set to black, base-300 changed
to #3b3a3a). Dracula is unloaded; monokai is the sole bundled theme and
default.
Also adds .env.development (dev ports 24203/24205) and .env.production
(arch ports 24991/24990) so Vite uses the correct backend per mode.
Excludes src/themes/ from biome (third-party CSS).
683 tests green.
|
|
1. Real context window: GET /models now returns modelInfo[model].contextWindow.
The Composer uses this instead of the hardcoded MAX_CONTEXT = 1,000,000.
Falls back to 1M when modelInfo is absent or the model has no contextWindow.
2. Percentage-based auto-compact: the compact-threshold endpoint is renamed
to compact-percent. The CompactionView now shows a percent input (0-100,
default 85, 0 = manual) instead of a token count input. Types renamed:
CompactThresholdResponse → CompactPercentResponse,
SetCompactThresholdRequest → SetCompactPercentRequest.
Note: the field name in the backend types is still 'threshold' (not
'percent') — the FE maps between them.
Re-mirrored .dispatch/transport-contract.reference.md.
686 tests green. 0 svelte-check errors + warnings.
|
|
Sidebar panel layout (which views are open and their order) and the
sidebar open/closed toggle are now persisted to localStorage. Default
layout is just the Model view at the top.
- ViewSidebar accepts an onChange callback that reports panel kinds
- App.svelte creates two createLocalStore instances (dispatch.sidebar.views
+ dispatch.sidebar.open) using the store's storage adapter
- AppStore exposes its storage instance so the shell persists via the
same adapter (test-injectable, not globalThis.localStorage)
- Tests pre-populate fake storage with ["extensions"] for the 4 tests
that need the Extensions view visible
686 tests green. 0 svelte-check warnings (2 pre-existing errors from
missing transport-contract exports, unchanged).
|
|
Tool calls and results now use the same DaisyUI collapse pattern as
thinking blocks — collapsed by default, click to expand. Each card
shows the tool name + a wrench icon in the title; expanding reveals
the input/output with overflow-x-auto for long lines and max-h-96
overflow-y-auto for very long output.
Batched tool calls: each entry is its own collapse card (was a DaisyUI
list). Pending results show a spinner in the title. Errors show a red
badge.
686 tests green.
|
|
Add overflow-hidden to tool card containers and overflow-x-auto to
<pre> elements so long tool output (file contents, JSON, etc.) gets
its own scrollbar instead of expanding the chat width and creating
a horizontal scrollbar on the entire transcript.
686 tests green.
|
|
trimTranscript now drops oldest provisional chunks (the in-flight turn)
when committed chunks are exhausted. Previously it bailed with drop=0
when committed was empty, allowing unbounded provisional growth during
long generating turns (300+ chunks → browser crawls).
Root cause of the syncTail approach failing: the kernel emits
step-complete (line 360) BEFORE calling onStepComplete (line 542) —
chunks are persisted only after tool results come back, not when
step-complete fires. So syncTail on step-complete found nothing.
Reverted the applyHistory + syncTail-on-step-complete changes from
4e1d041. The new approach is simpler: trim provisional directly in
trimTranscript. Dropped chunks are lost temporarily (no Show Earlier)
but come back as committed when the turn seals and syncTail fetches
everything from the server.
686 tests green.
|
|
The backend now persists chunks at step boundaries during generation
(CR-6). The FE calls syncTail on each step-complete event to fetch
the newly committed chunks. applyHistory clears the provisional array
when new committed chunks arrive mid-generation (they're duplicates
of what was folded from live events). The accumulating chunk (current
in-progress step) is kept.
This means trimTranscript can now drop oldest committed chunks
uniformly during a long turn — no unbounded provisional growth. The
browser never holds more than chatLimit chunks, even mid-generation.
3 new tests: clears provisional on new committed during generation,
keeps provisional when no new chunks, keeps accumulating when clearing.
689 tests green.
|
|
Backend shipped incremental seq assignment at step boundaries during
generation. Marked CR-6 as RESOLVED with the FE adoption plan (option c:
fold events for current step only, syncTail for sealed steps). Trimmed
the verbose resolved-CR history to a compact table — the code is the
source of truth now. 686 tests green.
|
|
Compacted conversations start with a system summary (role: "system")
instead of a user message (role: "user"). The interleaveTurnMetrics
function only detected segments by role === "user", so compacted
conversations got zero segments and no metrics were emitted.
Fix: when no user messages are found but metrics entries exist, treat
the entire transcript as one segment. This places turn-metrics at the
end and anchors step-metrics to any tool-batch groups by stepId.
686 tests green.
|
|
When stepIds are absent on persisted chunks (or don't match), the
sequential fallback was HEAD-aligning — matching the OLDEST entries
(trimmed turns) to the NEWEST segments. This showed 'turn 1' on turn 20's
content and placed the wrong metrics on the wrong segments.
Fix: when stepId matching produces ZERO matches, use TAIL-ALIGNMENT
instead — match the LAST T entries to the T segments (the loaded chunks
are always the newest). The oldest entries (trimmed turns) are unmatched
and emit standalone turn-metrics rows at the top.
686 tests green.
|
|
When the chat limit unloads a turn's content (user message + chunks),
the segment disappears and the turn-metrics row was lost. Now unmatched
entries (fully trimmed turns) emit a standalone turn-metrics row at the
top of the transcript, so the user still sees 'turn N · X tok' for
unloaded turns.
Note: trimming during generation only affects COMMITTED chunks (old
turns). Provisional chunks (the in-flight turn) are never trimmed — the
big trim happens at seal when provisional → committed. This is by design.
|
|
The turn number comes from the entry's position in the metrics array
(1-based), which is correct regardless of trimming since stepId matching
aligns segments to the right entry. Now displays 'turn 3 · 12k tok' instead
of just 'turn · 12k tok'.
|
|
Step-metrics are only shown when anchored to their tool content (inline
after the tool-call/result group). Steps whose chunks were trimmed (or
text-only steps with no tool chunks) are now SKIPPED instead of piling
up at the segment tail as empty 'step N · X tok' bubbles.
The turn-total metrics row still shows the aggregate (tokens, duration,
cache rate), so the conversation-level summary is preserved.
Updated place.test.ts + ui.test.ts to anchor steps with tool-call groups
where step-metrics are expected.
|
|
Vite define bakes __APP_VERSION__ (git rev-parse --short=5 HEAD) at build
time — survives bundling into the arch package deploy. Falls back to 'dev'
when not in a git repo. Also fixes two noNonNullAssertion warnings in
place.ts.
|
|
trimming
When the chat limit trims old chunks, head-aligning turn metrics to
segments by position breaks (a trimmed user message removes a segment
boundary, shifting all subsequent alignments by one). Fix: match segments
to metrics entries by stepId overlap (pass 1), falling back to sequential
matching for text-only segments with no stepId-bearing groups (pass 2).
This prevents step/turn metrics from being placed on the wrong segment
after trimming, while preserving the original behavior for text-only turns.
686 tests green.
|
|
One button to the right of the text input:
- idle → Send (starts a turn)
- generating + text → Queue (steers via chat.queue)
- generating + empty → Stop (aborts via POST /stop)
|
|
Consume the stop-generation handoff (no version bumps, no new types).
- App store: stopGeneration() → POST /conversations/:id/stop (fire-and-forget)
- Composer: stop button (square, error color) visible only while generating,
next to the send/queue button
- Existing event flow handles the rest: done with reason 'aborted' clears
generating; conversation.statusChanged: idle updates the tab spinner
686 tests green.
|
|
Consume the compaction handoff ([email protected], [email protected]).
Re-pinned file: deps + re-mirrored .dispatch/*.reference.md.
- New 'Compaction' sidebar view (CompactionView.svelte):
- 'Compact now' button → POST /conversations/:id/compact (loading indicator
+ result: 'N messages summarized, M kept')
- Auto-compact threshold number input → GET/PUT
/conversations/:id/compact-threshold (0 = disabled, default 350000)
- Re-mounts per conversation via {#key}
- App store: compactNow() + compactThreshold reactive state +
setCompactThreshold(), seeded on focus change (like reasoning-effort + cwd)
- conversation.compacted WS handler: reloads the SAME conversation's history
(ID unchanged — old history forked to an archive, not a tab switch)
- WS adapter parses newConversationId field on ConversationCompactedMessage
- conformance guards + tests cover the new type
686 tests green.
|
|
Consume the conversation lifecycle handoff ([email protected], [email protected]).
Re-pinned file: deps + re-mirrored .dispatch/*.reference.md.
- fetchOpenConversations() on connect: GET /conversations?status=active,idle
restores the tab bar across devices (merges with localStorage — opens new
tabs, removes closed ones, updates titles from backend)
- conversation.statusChanged WS handler: closed → removeTabLocally (no
re-POST); active → open tab + spinner; idle → update status map
- conversation.compacted WS handler: dispose stale store + cache, reload
history from server
- TabBar shows a spinner on active conversations (statusFor prop)
- closeTab refactored to use removeTabLocally (extracted cleanup)
- conformance guards + WS adapter tests cover all 3 new WsServerMessage types
686 tests green.
|
|
CLI --open should add the tab to the strip but leave the user on their
current tab. Add openTab reducer (add without selecting) + use it in
openConversation instead of createTab+selectTab.
|
|
|