| Age | Commit message (Collapse) | Author |
|
|
|
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.
|
|
The wake probe was hardcoded to claude-3-5-haiku-20241022, which the
endpoint no longer serves (HTTP 404), exhausting the retry loop. Now the
probe fetches the live model list via fetchAnthropicModels (falling back
to ANTHROPIC_MODELS_FALLBACK if empty) and selects the current Haiku via
a new pure selectHaikuModel() helper (first case-insensitive 'haiku'
substring match; newest-first ordering). No-match surfaces a clear
per-account error instead of crashing.
|
|
The wake probe POSTed a bare { model, messages } body with no system[]
identity. Anthropic validates system[] on OAuth (Pro/Max) subscription
requests and rejects any that lack the verbatim Claude Code identity, so
every scheduled wake (and the manual Wake-now button) failed silently —
surfacing as a blank '— failed' status that then burned the retry budget.
- Add pure buildWakeProbeBody(model) in @dispatch/core mirroring a genuine
Claude Code request (billing header block + identity block + 'hi'), with
a unit test for its shape.
- wakeAllClaudeAccounts now sends that body plus the CLI session/request-id
headers, and records 'HTTP <status>: <message>' on failure so the panel
never shows a bare 'failed' and breakage stays debuggable.
|
|
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).
|
|
Round-2 Gemini review surfaced that the toggle endpoint derived add-vs-
remove from its own in-memory state, which combined catastrophically
with any UI desync: a user clicking to turn ON an hour the UI showed as
off, but the server had as on, would silently get the hour turned OFF.
The clicks felt 'inverted' and the only recovery was a full reload.
Fix: require an explicit `action` field on every /toggle request. The
client must declare its intent; the server is no longer allowed to guess.
Idempotency rules:
- action: 'off' on an already-off hour → 200, no-op success.
- action: 'on' on an already-on hour → 200, REPLACES timestamps (so a
recovering UI can re-assert the user's wall-clock intent without a
delete-then-add round trip).
- Missing or invalid action → 400.
The 'off' path no longer reads or requires `timestamps`. The 'on' path
still requires all four slot timestamps as finite Unix-ms numbers (the
skewed-toggle relaxation from round 1 is preserved).
Tests:
- toggle() helper auto-derives action from `timestamps` presence, so
the existing 12 tests stayed terse. One test that relied on the old
'empty body = add' behavior now passes `action: 'on'` explicitly.
- Added 4 new contract tests:
* rejects requests missing/with-invalid action
* action='off' on an already-off hour is idempotent
* action='on' on an already-on hour replaces timestamps (the
round-2 desync-recovery scenario)
* action='off' ignores stray timestamps payloads
29 / 29 routes tests pass; 431 / 431 across the workspace.
|
|
boot-recovery reason
Three review-finding fixes in models.ts + regression tests:
1. POST /wake-schedule/toggle no longer rejects 'past' timestamps
(Gemini #1, High). Client-server clock skew + request latency
meant a freshly-computed nextOccurrenceAt(HH:MM) for an imminent
slot could land in the past by the time the server validated it,
silently failing the UI toggle. The scheduler's recoverScheduleEntry
already fires within MISSED_WAKE_GRACE_MS and rolls forward by
24h × N, so the strict <= now check was actively harmful. Kept
Number.isFinite + slot-present validation.
2. persistSchedule is now transactional (Gemini #3, Medium). The old
DELETE-then-N-INSERTs path, when an INSERT failed mid-loop, left
the table empty (DELETE had committed) and silently wiped the
user's schedule on next boot — the catch swallowed the error.
Wrapped both in db.transaction(...): on failure everything rolls
back, in-memory state is untouched, and the previously persisted
snapshot stays intact.
3. Boot-recovery reason no longer masked when boot recovery + due
slots coincide (Gemini #5, Nit). Capture bootFireRequested before
clearing the flag and append ' (boot recovery)' to the reason
so the lastWake/pendingRetry surface tells the truth.
Tests:
- Replaced 'POST toggle rejects past timestamp' (the bug-as-feature
test) with 'POST toggle ACCEPTS a slightly-past timestamp (clock
skew / latency)' regression guard.
- Added 'POST toggle rejects NaN / Infinity / non-number slot values'
to lock the malformed-input path.
- Added 'snapshot remains consistent across toggle round-trips
(persistSchedule atomicity)' — exercises GET/POST cycles to ensure
the transactional impl agrees with itself across add/remove.
All 427 tests pass; biome clean.
|
|
same-tick fires
Marking an hour on the Claude Wake Schedule panel now schedules FOUR probes
within that hour instead of one. Rate-window edges are unforgiving — a
single probe at :15 can miss the actual reset moment by up to 14 minutes;
hitting :00 / :15 / :30 / :45 puts us within ~7 minutes of any reset that
happens during that hour.
When multiple slots come due in the same 30s scheduler tick (or recover
together at boot), they coalesce into a SINGLE upstream wake call — no
point hitting Anthropic 4× in the same window.
DB schema
- wake_schedule is now (hour, slot_minute, next_wake_at) PK (hour,
slot_minute). Destructive migration: detect old single-row-per-hour
schema by absence of the slot_minute column and DROP TABLE. No other
table is touched. Per user direction: no back-compat for old rows.
API
- POST /models/wake-schedule/toggle add: { hour, timestamps: { '0': ms,
'15': ms, '30': ms, '45': ms } } — all 4 slots required, all must be
future Unix ms. Delete shape unchanged ({ hour }).
- GET /models/wake-schedule shape:
schedule: { '9': { '0': ts, '15': ts, '30': ts, '45': ts }, ... }
probeSlotMinutes: [0, 15, 30, 45]
resetOffsetHours, lastWake, pendingRetry (unchanged from prior commit)
Frontend
- Computes 4 timestamps client-side (next occurrence of HH:MM in local TZ)
and sends them in one request.
- markedHours summary now says 'Probes :00 :15 :30 :45 → reset by ~Xh later'.
- Same in-flight tracking / current-hour ring / status row as before.
Tests
- wake-scheduler.test.ts unchanged (pure helpers still correct; added
PROBE_SLOT_MINUTES + isProbeSlotMinute exports).
- routes.test.ts rewritten for the new payload shape: 12 wake-schedule
tests covering snapshot shape, add/remove (full 4-slot round-trip),
validation (range, integer, past-slot, missing slot, non-object,
missing timestamps), independent multi-hour scheduling, and
re-toggle replacement. 417 tests total (was 414).
|
|
status surface
Bugs fixed
- Missed wakes silently lost. The old loadScheduleFromDB just pushed any
past next_wake_at to its 'next occurrence' in *server* local time, so a
wake that fired while the API was down never ran — defeating the whole
point of the panel (overnight task picks up after a 5h rate-window
reset). Now: if missed by <= 2h we fire it on the next tick; either way
the entry is rolled forward by 24h-multiple steps.
- Server-TZ drift. nextOccurrenceAt15 used the server's local TZ, so on
a UTC Docker host running for a user in PST the reschedule slowly
migrated the fire time. Now we advance by 24h * N from the original
client-supplied timestamp, preserving the user's wall-clock intent.
- Retry storm. Every failed wake pushed a new entry into a retries[]
array, all converging at the same +5min instant. Replaced with a single
shared pending-retry slot whose budget resets on subsequent failures.
- Retry race with fresh fires. If a tick fired AND a retry was due in
the same iteration we'd double-hit the upstream. Now retries only run
on ticks where no fresh wake fired.
New behavior surfaced on /wake-schedule:
{ schedule, resetOffsetHours, lastWake, pendingRetry }
POST /wake-schedule/toggle now also rejects non-integer hours (4.5, etc.)
and returns the same snapshot shape so the client can stay in sync.
Tests: 9 new HTTP route tests covering snapshot shape, add/remove,
validation (range, integer, past timestamp, missing timestamp), and
independent multi-hour scheduling.
|
|
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
|
|
- 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
|
|
- Add SQLite database at ~/.local/share/dispatch/dispatch.db with tables: credentials, api_keys, wake_schedule, usage_cache
- Store Claude OAuth credentials in DB with import button in Model Status UI
- Store OpenCode/Copilot API keys in DB with paste-to-import modal
- Store OpenCode cookie and workspace IDs in DB
- Migrate wake schedule from .wake-schedule.json to DB
- Migrate usage cache from in-memory Map + localStorage to DB
- Remove all env var and file fallbacks — DB is the single source of truth
- Add seed scripts: bin/import-credentials.ts, bin/seed-opencode-keys.ts
- Docker: container runs as host UID/GID with matching home directory
- Clean up dispatch.toml: remove env fields, update comments
- Progress bar time markers for usage cycle tracking
|
|
display names
- Wake scheduler: fix Bun timer leak, make recurring daily, persist to disk, retry failed wakes every 5min for 30min, start at boot
- Key usage: localStorage cache survives page refresh, spinner during all refreshes, show cached data immediately
- Credential filtering: key-usage and wake only use configured credentials_file, exclude unconfigured accounts
- Display: remove counter suffix from Claude labels, format opencode/copilot key names
|
|
- Replaced POST /wake-schedule (full-replace) with
POST /wake-schedule/toggle (atomic single-hour toggle)
to eliminate race conditions between frontend and scheduler
- Recursive setTimeout prevents overlapping wake executions
- HMR-safe via global timer reference
- Frontend now uses toggle endpoint instead of full schedule POST
- Display shows reset time (wake hour + 5h) in American 12h format
e.g. '8:15 → Reset at 1:00 PM' instead of European 24h
|
|
- Added claude-pro key pointing to default credentials, claude-max
pointing to .credentials-2.json (docker path /root/.claude/)
- POST /models/wake sends 'hi' to haiku for all Claude accounts
- ClaudeReset.svelte: 2 AM rows + 2 PM rows of 6 hour blocks each
(12-hour American format). Click blocks to schedule wake at :15
- Key Usage now groups all Claude accounts under one 'Claude' card
instead of duplicating under claude-pro and claude-max cards
|
|
- /key-usage route now returns all Claude accounts (not just first)
- Added ClaudeAccountUsage type for per-account data
- Frontend shows all keys stacked in scrollable cards
- Each Claude account rendered separately with label + subscription badge
- Removed key selector dropdown
|
|
Adds 'Key Usage' to the sidebar dropdown with per-provider live usage:
- Claude: 5-hour and weekly utilization bars with reset timestamps
(normalizes Anthropic's 0-100% API response to 0-1 internally)
- OpenCode: Scrapes usage from workspace page via OPENCODE_COOKIE
session cookie, mapping key IDs to OPENCODE_WS1_ID/OPENCODE_WS2_ID
- Copilot: Fetches from api.github.com/copilot_internal/user with
entitlement/remaining/quota reset tracking
New files: opencode.ts, copilot.ts (usage fetchers), KeyUsage.svelte
New route: GET /models/key-usage?keyId=X dispatches by provider
|
|
effort, and dynamic model listing
|
|
- 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)
|