| Age | Commit message (Collapse) | Author |
|
|
|
# Conflicts:
# packages/api/src/agent-manager.ts
# packages/api/tests/agent-manager.test.ts
# packages/frontend/src/lib/tabs.svelte.ts
|
|
Keep a tab's provider prompt-cache warm while idle by periodically replaying
the exact cached conversation prefix plus a single trivial throwaway turn,
resetting the provider's ~5-min cache TTL so the user's next real message hits
a warm cache.
Backend:
- Agent.warmCache(history): extracts buildLlmContext() shared with run(), then
re-sends the identical system+tools+history prefix (same Anthropic
cache_control breakpoints) plus a 'reply with just a .' probe turn via
toolChoice:none. Returns the request usage; mutates no history, emits/persists
nothing.
- AgentManager.warmCacheForTab(): resolves the same agent the next real turn
would use, replays the FULL genuine history, refuses while a turn is running.
- POST /chat/warm: returns ONLY the warming request's usage (never persisted,
never folded into the real usage aggregate).
Frontend:
- cache-warming.svelte.ts store: per-tab 4-min repeating idle timer with
countdown, warming-specific last-request cache %, and error capture. Arms on
turn end, pauses during a turn, disables+resets on a real user message.
- cache-warm-storage.ts: per-tab localStorage persistence of the toggle.
- Lifecycle hooks wired into tabs.svelte.ts (status/statuses/sendMessage/
hydrate/create/open/close).
- ModelSelector: bottom-of-panel checkbox + debug strip (last-% / countdown /
error), shown only when enabled. Warming cache data never touches the real
Cache Rate view.
Tests: core warmCache (5), api warm route (3) + warmCacheForTab (3), frontend
store (12) + storage (10). check / test (779) / frontend build / typecheck all
green.
|
|
# Conflicts:
# packages/frontend/src/lib/components/ChatInput.svelte
|
|
Summarize a conversation's older "head" into a structured anchored Markdown
summary while preserving the most recent turns verbatim, shrinking context size
while keeping the information needed to continue coherently. Triggered by a
"Compact conversation" button in Chat Settings (not an agent tool).
Approach informed by OpenCode's session/compaction.ts:
- Ported SUMMARY_TEMPLATE (Goal / Constraints / Progress / Key Decisions /
Next Steps / Critical Context / Relevant Files) and the anchored-summary
buildPrompt (re-summarizes a prior summary when present).
- Ported the TOOL_OUTPUT_MAX_CHARS (2000) cap on tool results in the summary
request.
- Simplified tail selection to a fixed recent-turn count (DEFAULT_TAIL_TURNS=2)
instead of OpenCode's token-budget splitTurn.
core:
- New src/compaction/ module (pure, DB-free): template, prompt builder,
head/tail selection, transcript renderer with tool-output capping, prior
summary extraction. Generic over ChatMessage so callers keep turnId/seq.
- db/chunks.ts: rekeyChunks(from,to) relocates a tab's full history to a
backup tab (reversible — nothing is deleted).
- AgentEvent: compaction-started / -complete / -error variants.
api:
- AgentManager.compactTab(tempTabId, sourceTabId): side-effect-free
resolveConnection() for the compactor model (configured compaction_model_*,
else the source tab's own key+model), one-shot tool-less summary generation
via a transient Agent, then relocate full history to a fresh backup tab and
re-seed the canonical source id with [summary turn + preserved tail]. Source
tab is locked (messages queue) during the run; queue drains afterward.
- Routes: POST /tabs/:id/compact, GET/PUT /tabs/settings/compaction-model.
frontend:
- "Compact conversation" button in ModelSelector (Chat Settings), between
Working Directory and the agent toggle; idle-gated.
- Compaction-model key+model selector in Settings, beside the title model.
- Transient placeholder tab shows a large, non-faded "Please wait, compacting
conversation…" screen; closing it cancels. Source input locked while running.
- Handle compaction-* events: reload compacted source, insert backup tab,
refocus source, discard placeholder.
tests: core compaction unit tests, rekeyChunks DB test, AgentManager.compactTab
orchestration tests, and compaction route tests. All green (713 tests), biome
clean, all typechecks pass, frontend builds.
|
|
Add multimodal image/PDF input to the chat box via clipboard paste, gated by a
graceful per-model capability check.
UX: a pasted image/PDF inserts an inline token (【image:…】 / 【pdf:…】) into the
draft, so attachments have ORDER relative to typed text and can be referenced
positionally. The token is the only handle — deleting it (atomic Backspace/
Delete, or selection overlap) detaches the file; an input-reconciliation safety
net detaches any attachment whose token is no longer intact. No preview strip.
Capability check: resolveModelCapabilities reads models.dev modalities.input
(new GET /models/capabilities, mirrors /context-limit). The input blocks Send
(no tokens spent) only on a definitive 'no'; unknown capability (catalog offline
/ unmapped provider) stays permissive. Attachments require a fresh turn — Send is
blocked while generating and /chat rejects content mid-turn (409).
Attachments are EPHEMERAL: forwarded to the model for the turn via ordered AI SDK
ImagePart/FilePart content, but never persisted (history keeps the text with
[image]/[pdf] markers). Text-only turns serialize byte-identically to before.
Limits (Anthropic-aligned, enforced at paste + re-validated server-side):
PNG/JPEG/WebP/GIF/PDF; image ≤5MB, PDF ≤32MB, ≤20 attachments, ≤32MB total.
core: UserContentPart types, models/attachments validator, capability resolver,
agent.run+toModelMessages thread ordered content. api: /chat content validation +
passthrough. frontend: attachment-tokens helper, ChatInput paste/token/gating,
per-tab staged attachments, App.svelte capability fetch. +44 tests.
|
|
Restructure ChatInput into two stacked bars:
- Top bar: auto-resizing textarea + fixed-width send/stop button that
morphs in place (no layout shift) across idle/generating states.
- Bottom bar: agent status icon, context-window fill bar, and compact
token count + percent (inert bar when model max is unknown).
Wire contextLimit prop from App.svelte into ChatInput, reusing the
shared computeContextUsage helper so it agrees with the sidebar.
|
|
# Conflicts:
# packages/api/tests/agent-manager.test.ts
|
|
Add a 'Context Window' sidebar view showing the live context occupancy
(latest request's input+output) against the model's maximum context
window, resolved dynamically from the models.dev catalog.
- core: models.dev catalog module (resolveContextLimit) with disk cache,
TTL, stale-fallback + offline penalty memo; null for unknown models.
- api: GET /models/context-limit?provider=&modelId=.
- frontend: ContextWindowPanel + computeContextUsage helper; App resolves
+ caches the active model's max (anthropic/opencode-anthropic only);
percent shown to 2 decimals; degrades to bare token count when max
unknown.
- tests: core catalog (13), api route (3), frontend helper (6).
|
|
Add a per-model/key reasoning effort setting to agent definitions,
surfaced and editable in the Agent Settings page and displayed at a
glance in the model selector views.
- core: single source of truth for effort levels (REASONING_EFFORTS,
DEFAULT_REASONING_EFFORT='high', labels, isReasoningEffort guard);
add 'xhigh' level; AgentModelEntry.effort; xhigh budget=24000 for
classic-thinking Claude; default floor 'high'. Persist/parse effort
in the agent TOML loader.
- api: thread effort through the fallback chain with per-model -> per-tab
-> default precedence; validate /chat + agentModels effort from the
canonical list.
- frontend: effort <select> per model row in AgentBuilder; effort badges
in ModelSelector (agent + subagent chains); Thinking dropdown sourced
from canonical list; per-tab default raised to 'high'.
- tests: +15 (loader round-trip, agent xhigh budget, canonical list +
guard, api precedence, route validation).
|
|
Gemini review surfaced that App.svelte (onMount theme apply) and
SettingsPanel.svelte (theme <select>) hand-rolled their own defaults
and could disagree:
- App.svelte only set data-theme if localStorage had a value, so on a
fresh install daisyUI fell back to the first theme in app.css (light).
- SettingsPanel.svelte hardcoded a UI default of "dark".
Result: a first-time user saw a light app but a Settings panel that
claimed "dark" was selected. Picking *any* value in the dropdown was
the only way to reconcile reality with the UI.
This commit:
- Adds packages/frontend/src/lib/theme.ts as the single source of truth:
THEMES list, Theme type, THEME_STORAGE_KEY, DEFAULT_THEME, plus
loadStoredTheme() and applyTheme() that handle SSR / private-mode /
bad-value cases.
- Rewires App.svelte's onMount to call applyTheme(loadStoredTheme()),
so the boot apply always writes a known good theme to the DOM (even
on fresh installs), matching what Settings will show.
- Rewires SettingsPanel.svelte's picker to use the shared module,
dropping its duplicate THEMES const, duplicate storage key, duplicate
apply/persist logic, and the conflicting "dark" fallback.
- Adds 11 unit tests in tests/theme.test.ts covering the
default-fallback, known/unknown stored values, SecurityError-on-read,
SSR (no localStorage), DOM-attribute write, persistence round-trip,
and the "DOM still updates if storage write throws" contract.
The daisyUI plugin block in app.css still lists themes — that's a
CSS-time concern and can't be imported from TS, so it's kept in sync
by convention (noted in the new module's doc comment).
|
|
- send prompt-caching + oauth anthropic-beta headers on the Claude OAuth provider
- restructure the OAuth request body (billing header, identity split, relocate
third-party system prompt to the first user message) to match Claude Code
- apply rolling cache_control breakpoints and group a turn's tool results into a
single role:tool message for correct breakpoint placement
- emit per-step usage events (cache read/write split) and add the Cache Rate
sidebar panel
- dedup byte-identical tool calls within a single batch
|
|
AgentBuilder default, SubAgent mode display
- Filter summon tool catalog to is_subagent-flagged agents only
- Return fresh subagent list in error when slug not found
- Add subagent hint to system prompt when summon tool available
- Default is_subagent checkbox to true in AgentBuilder
- Fix tab-created event to include agentSlug and agentModels
- Add SubAgent read-only mode to ModelSelector with model slider
|
|
running in background
Implements the 'background-running agents + restore-layout-on-reopen'
feature. Full design and parallel-implementation plan in
`plan-bg-restore.md`; Gemini code review (SHIP verdict, no findings) in
`report.md`.
User-visible behaviors:
1. Browser-close keeps agents alive. If an agent is mid-stream when
the browser closes / reloads / loses the network, it continues
processing on the backend. (This was already the case in code —
agents run fire-and-forget in app.ts:77-79 — but it was previously
pointless because the UI never restored the tab to receive the
output.)
2. Layout restore on browser reopen. Every tab that existed at the
time the window was closed is restored, in original `position`
order, with full persisted message history. Tabs whose agents
finished while disconnected appear with the completed message.
Tabs whose agents are still running appear streaming live — the
in-flight assistant message is reconstructed from the backend's
in-memory `currentChunks` (sent over the wire on connect) and
accumulates new deltas as they arrive.
3. Explicit tab-close cancels + forgets. Clicking the X still
cancels the agent (existing `stopTab` in DELETE /tabs/:id) and
archives the row (`is_open = 0`), so it is not restored. No
change to that path.
The gap that the implementation closes: previously, App.svelte:onMount
unconditionally called `createNewTab()` with a fresh UUID, ignoring
every existing row in the `tabs` table. Every browser open was a
clean slate. The DB had the conversation history but no way for the
UI to discover it.
Implementation:
• New `TabStatusSnapshot` interface in
packages/core/src/types/index.ts (auto-exported via existing
`export * from "./types"`):
interface TabStatusSnapshot {
status: AgentStatus;
currentChunks?: Chunk[]; // present iff running
currentAssistantId?: string; // present iff running
}
• `agent-manager.ts:getAllStatuses()` rewritten to return
`Record<string, TabStatusSnapshot>` (was
`Record<string, AgentStatus>`). For running tabs only, attaches a
defensive shallow copy of `tabAgent.currentChunks` (the live
streaming array the per-message loop appends to) plus the DB id
of the in-flight assistant message. The defensive copy is the
consumer's to mutate. Idle / error tabs get `{ status }` only.
`GET /status` and the WS `onOpen` snapshot both pick up the new
shape automatically — neither call site changed.
• Frontend mirror of `TabStatusSnapshot` in
packages/frontend/src/lib/types.ts; `AgentEvent.statuses` variant
updated to use `Record<string, TabStatusSnapshot>`.
• New `hydrateFromBackend()` on the tab store
(packages/frontend/src/lib/tabs.svelte.ts). Sequence on app
mount:
1. Bail with 0 if `tabs.length > 0` (hot-reload idempotency).
2. GET /tabs → list of `is_open=1` rows in
`position` order.
3. GET /status → in-flight TabStatusSnapshot map.
4. GET /tabs/:id/messages for each tab in parallel via
Promise.all → persisted ChatMessage[].
5. Build the Tab objects, splicing the snapshot's live
chunks into the in-flight assistant message for every
running tab (two paths: merge into the existing DB row
with matching id, or append a fresh in-flight message
if no row matches).
6. `tabs = restored; activeTabId = restored[0]?.id ?? null;`
Every fetch is wrapped in try/catch so one tab's failure can't
destroy the whole restore pass.
• WS `statuses` handler in `tabs.svelte.ts:handleEvent` rewritten
for the new shape. Still fires `reloadTabMessagesFromApi` on the
desync case (frontend thinks running, backend says idle — the
pre-existing recovery path is preserved). When backend says
running, seeds in-flight chunks into the assistant message
matching `snap.currentAssistantId` (creating it if needed).
When backend says non-running, clears `isStreaming` on the
previous in-flight message and nulls `currentAssistantId`.
• `App.svelte:onMount` now awaits `tabStore.hydrateFromBackend()`
before deciding whether to fall back to `createNewTab()`.
Fallback condition is the doubly-defensive
`restored === 0 && tabStore.tabs.length === 0`. `wsClient.connect()`
fires in parallel with hydration — the resulting WS `statuses`
event is per-tab idempotent against the hydrated state, so there
is no race even if it arrives mid-hydration.
What was NOT done (deliberately, deferred to wishlist):
• Pre-existing inconsistency: core `AgentStatus` includes
"waiting_for_key" but frontend `TabStatusSnapshot.status` uses
only the existing 3-state pattern ("idle" | "running" | "error").
Not introduced here; mirrored the existing precedent.
• Restored tabs use defaults for `reasoningEffort`, `agentSlug`,
`agentScope`, `agentModels`, `workingDirectory` — these are not
in the DB `tabs` schema. Future schema expansion.
• Per-delta DB flushing — not needed; the in-memory snapshot
covers the gap between flushAssistant calls.
• LocalStorage cache of tab ids — backend DB is the source of truth.
Process notes:
• Implemented via parallel programmer subagents (flash agents were
requested but unavailable in this environment — substituted with
"programmer" agents, which share the "reads a plan, implements a
single step" charter). Backend (Segment A: getAllStatuses + 5
tests) and frontend (Segment B: types + hydrateFromBackend +
statuses handler + onMount + 8 tests) ran disjoint-file-ownership
in parallel.
• Gemini code review (yolo mode for tool access, explicit
prompt-level write restriction to `report.md` only) returned a
SHIP verdict with no findings against the plan.
• Self-review surfaced one followup gap that Gemini's earlier
plan-mode pass also caught: no explicit test for
`/tabs/:id/messages` failure isolation. Added a test covering
both HTTP-500 and network-error variants alongside a healthy
tab, asserting per-tab failures don't destroy the whole restore.
Tests:
• api/tests/agent-manager.test.ts: +5 (snapshot empty record,
idle-tab field omission, running-tab field inclusion, defensive
copy invariant, omits chunks for running tab with null
currentChunks). 31 total (was 26).
• frontend/tests/chat-store.test.ts: +9 (restore-with-messages,
in-flight seeding, /tabs failure → 0 returned, empty /tabs
array, idempotency when tabs already exist, idle-status when
/status omits, running-snapshot statuses handler seeding,
idle-snapshot statuses handler clearing, per-tab failure
isolation across HTTP-500 and network-error). 44 total (was 35).
Totals: 243 tests across 3 packages all green; typecheck clean on
core + api + frontend; biome clean across 124 files.
|
|
filter
- Added Google (Gemini) as a provider: add-key UI, env var resolution via resolveApiKey, usage tracking via native models endpoint + gemini.google.com cookie scraping
- @ai-sdk/anthropic upgraded to v3 (adaptive thinking support) with LanguageModelV1 cast for ai v4 compat
- Claude Opus 4.7 uses adaptive thinking (type: adaptive); all other models keep explicit budget tokens
- Model selector modal: search filter with space matching dash/underscore
- Copy button: all tool results truncated at 300 chars
- Sidebar layout fix: Claude Reset panel removed from flex-1 fill to prevent overlap
|
|
- Add is_subagent checkbox to agent editor; subagents are hidden from Chat Settings
- Add is_subagent field to AgentDefinition type, TOML serialization, and API route
- Filter subagents from ModelSelector agent list
- Fix all biome lint/format errors across codebase (useLiteralKeys, noNonNullAssertion, noExplicitAny, formatting, import sorting)
- Fix svelte-check errors (type narrowing in SkillsBrowser, ToolPermissions, SidebarPanel)
- Fix a11y warnings in App.svelte (label-control associations)
- Fix test mocks missing BackgroundShellStore, BackgroundTranscriptStore, createWebSearchTool, createYoutubeTranscribeTool
- Update stale 409 test to match current message-queuing behavior
- Exclude packaging/ and release/ dirs from biome to avoid linting stale build artifacts
|
|
- Add POST /models/add-key and POST /models/remove-key API endpoints
- Add 'Add New Key' modal (page-level) with provider selection
- Add remove button per key in Model Status view
- Add configurable backend URL setting in Settings panel with localStorage persistence
- Convert systemd service from system to user service (systemctl --user)
- Fix Docker entrypoint to chown all nested node_modules dirs
- Update dispatch.toml credential paths to use -pro/-max naming
- Make API port configurable via PORT env var (default 3000, prod 18390)
|
|
handling
- Agent Builder: full CRUD with card grid, drag-and-drop model reorder, edit/delete
- Auto-save on edit with 600ms debounce, AbortController for concurrency, fieldset disabled until name entered
- Agent definitions stored as TOML with cwd field, loaded from global/project dirs
- Working directory: per-tab CWD override in Chat Settings, agent default CWD, auto-create on first message
- CWD validation: check-dir endpoint with ~ expansion, real-time validity indicator
- Subagent CWD validated against parent's effective CWD using path.relative
- Unavailable tool calls: caught gracefully, shown as tool call with error badge, model retries
- UI: tab bar border radius, sidebar border removed, chat input ghost style, scroll-to-bottom rectangle
- Skills dir collapse uses CSS rotation, Model Choice renamed to Chat Settings, System Prompt view removed
- Reusable SkillsBrowser/ToolPermissions with external mode for Agent Builder
- ModelSelector: Agent/Manual toggle, agent list, Agent Settings link
- Page router, skills recursive scanning, bin/up gopass removed, docker volume mounts
|
|
sidebar, UI polish
- System Prompt sidebar view: editable textarea, save-on-send, reset button
- Tool permissions: save-on-send pattern (not immediate), reset button
- Dynamic system prompt: buildSystemPrompt reads from DB, tool list auto-generated
- Responsive sidebar: overlay on small screens with backdrop
- Chat bubbles: user=fit-width, assistant=full-width
- Fix infinite loops (use onMount for data fetching)
- Fix sendMessage race condition (await settings saves before chat POST)
- Model selector: auto-open model modal after key selection
- Rename views: Permissions->Tools, Tab Settings->Model Choice
- Shared appSettings store for cross-component reactive state
- Delete old chat.svelte.ts
|
|
- Add tabs, messages, and settings tables to SQLite database
- Backend: refactor AgentManager to manage per-tab Agent instances via Map<tabId, TabAgent>
- Backend: WebSocket events tagged with tabId for multiplexing
- Backend: tab CRUD routes (create, list, update, archive, messages)
- Backend: persist user and assistant messages to DB during chat
- Frontend: new tabStore replaces single chatStore with multi-tab reactive state
- Frontend: TabBar component using DaisyUI tabs-lift style with status dots
- Frontend: Settings sidebar panel for title generation model selection
- Frontend: wire ChatPanel, ChatInput, Header to use tabStore
- Fix HMR listener accumulation via wsClient.clearCallbacks()
- Delete old single-chat chatStore (chat.svelte.ts)
|
|
- Remove ModelResolver, model definitions, model tags, fallback order, agent templates
- Remove all [[models]], [agents], and fallback from dispatch.toml and config schema
- ModelRegistry is now a pure key-state manager
- dispatch.toml reduced to keys + permissions only
- Docker: fix entrypoint for existing UID, skip bun install in frontend container
- Docker: scoped build cache prune in bin/clean
|
|
|
|
|
|
effort, and dynamic model listing
|
|
inset-0
|
|
- Sidebar uses absolute inset-0 with overflow-y-auto for proper scrolling
- Border moved to inner div so it hides with sidebar
- Copilot auth script uses portable sed instead of grep -P (macOS compat)
- dispatch.toml with 3 keys, 4 models, fallback order
- .env.dispatch with key placeholders and script reference
- docker-compose loads .env.dispatch via env_file
|
|
- Config system: TOML-based dispatch.toml with hot-reload via chokidar
- Model/key resolution: tag-based model selection, key fallback chains
- Skills system: directory loader with TOML frontmatter, agent mappings
- Task list tool: add/update/list/get operations with WebSocket events
- API routes: GET /config, /skills, /skills/:name, /models, /models/resolve
- Frontend: sidebar with model status, task list, config viewer, skills browser, permission log
- Sliding sidebar animation using CSS transitions (not Svelte transitions)
|
|
|
|
Permission engine:
- Rule-based engine: wildcard matching, last-match-wins, reject cascade
- PermissionService with pending/approved state, PermissionChecker interface
- dispatch.yaml config loader with per-permission pattern rules
Shell tool:
- run_shell tool with child_process spawn, timeout, streaming output
- Tree-sitter static analysis (web-tree-sitter + tree-sitter-bash WASM)
- BashArity command normalization for 'always allow' patterns
- FILE_COMMANDS set: rm, cp, mv, mkdir, ls, find, grep, cat, etc.
Agent loop refactored:
- Removed maxSteps, manual step loop with tool execution
- Permission checks on shell commands (external_directory only)
- Permission checks on file tools outside workspace boundary
- Symlink bypass fix (realpathSync), .. false positive fix
- Shell output streaming via Promise.race + setImmediate polling
API layer:
- PermissionManager wraps PermissionService, broadcasts via WebSocket
- WebSocket handles permission-reply messages from frontend
- Config loaded from dispatch.yaml, converted to ruleset
Frontend:
- Permission prompt modal (native dialog, focus trap, ARIA)
- Always-allow confirmation flow with pattern preview
- Shell output display (live streaming + final parsed result)
- Permission log panel (fixed bottom-right overlay)
- Exit code badge (green 0, red non-zero)
134 tests, typecheck clean on all 3 packages
|
|
- Bun monorepo with @dispatch/core, @dispatch/api, @dispatch/frontend
- Agent runtime with Vercel AI SDK, streaming via WebSocket
- Tools: read_file, write_file, list_files (scoped to working directory)
- Hono API server with POST /chat, GET /status, GET /health, WS /ws
- Svelte 5 + DaisyUI frontend with chat UI, theme switcher, copy button
- OpenCode Go (Zen) as LLM provider, deepseek-v4-flash-free model
- Docker setup (dev + prod) with bin/ scripts and gopass secrets
- Biome v2 linting/formatting, Vitest tests (44 passing)
- Debug info attached to error messages for diagnostics
|