* feat: add service-managed self-update
* chore: add temporary self-update test scripts
* chore: simplify temporary script markdown fences
* chore: run frontend in temporary self-update scripts
* fix: show self-update job progress after apply
* chore: add real npm self-update test script
* fix: isolate npm self-update prefix
* fix: detect npm service installs from bin shim
* chore: reset service before real npm update test
* chore: expand real npm status diagnostics
* fix: preserve service home during self-update
* fix(cli): tee self-update helper output to a log file
* fix(cli): avoid launchd bootstrap race when reloading the macOS service
* fix(backend): run macOS self-update helper once via transient launchd plist
* fix(web): show self-update progress and lock the apply button until the update lands
* fix(cli): reinstall via the kandev wrapper on Homebrew self-update
* chore: remove temp self-update test scripts
* fix(web): drop unused apply path from useUpdates hook
* fix(backend): snapshot install state once in apply preflight
* docs: align self-update docs with transient LaunchAgent flow
* chore(release): run cli bundle from apps workspace root
* fix(backend): guard against concurrent self-update applies
* fix(backend): check scheme and helper paths in self-update apply
* fix(backend): drop duplicate binary name from npx manual command
* fix(cli): allow --help for service self-update without --intent
* fix(cli): harden self-update dir perms and darwin uid resolution
* fix(web): set required POST fields last in applyUpdate
* docs: use JSON body notation for the apply endpoint
* fix(backend): expire self-update guard so a stuck helper cannot wedge apply
* refactor(cli): collapse redundant writeSync branch in self-update log
* feat(web): show download progress for voice mode Whisper Web model
First-time use of voice mode with the Whisper Web engine downloads
~40–240 MB of model files before the first transcription. The existing
modelLoad progress was only surfaced via the mic-button tooltip, so the
download felt silent — users couldn't tell whether it was progressing,
stuck, or how much was left.
Adds a compact inline indicator next to the mic button that renders a
percent label and a Progress bar while the model is downloading, and a
short error label when load fails. The indicator stays hidden for the
Web Speech and server-Whisper engines (which don't download models) and
clears once the model is ready.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(web): tidy VoiceModelLoadIndicator class names and a11y
- drop unused cn() import (single-string className doesn't need it)
- drop redundant max-w-[8rem] (w-32 already equals 8rem in tailwind)
- include modelLabel in the error state's aria-label so screen readers
identify which model failed
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(web): a11y improvements for VoiceModelLoadIndicator
- Replace two separate loading/error divs with a single stable role="status"
wrapper so AT announces the loading→error transition (newly-inserted live
regions are silently ignored by most screen readers).
- Move numeric progress to role="progressbar" + aria-valuenow/min/max on the
inner div; removes per-tick live-region announcements.
- Drop redundant aria-live="polite" (role="status" implies it per ARIA spec).
- Align error visible text with aria-label: both now use modelLabel
("Whisper Base failed to load") instead of the generic string.
- Standardize tooltip wording to match indicator: "Downloading {model}…"
instead of "Loading model…".
- Update test: error branch expects model-specific label.
* refactor(web): fix nested progressbar and clamp tooltip percent
- Remove role="progressbar" + aria-value* from the wrapper div; <Progress>
(Radix) already renders its own progressbar — the nested node confused AT.
Pass aria-label directly to <Progress> for the accessible name.
- Drop outer aria-label on the loading state (Radix progressbar carries it).
- Clamp tooltip percent in voice-input-button.tsx to 0–100 and handle NaN
(mirrors clampPercent in the indicator, keeps UI consistent on edge values
from transformers.js).
- Update test: assert aria-label via getByRole("progressbar").getAttribute().
* fix(web): guard tooltip percent against NaN; assert error aria-label in test
- Tooltip loading percent: add Number.isFinite guard so NaN from
transformers.js progress produces "0%" instead of "NaN%".
- Test: assert aria-label on error state root matches modelLabel.
* fix(web): add w-32 to error variant to prevent layout shift on state change
Loading branch uses w-32 to hold a stable 128px slot in the toolbar.
Error branch lacked it, so the mic button shifted on loading→error transition.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat: add voice mode for chat input
Adds a voice input button to the chat composer with three transcription engines:
in-browser Web Speech API, in-browser Whisper via @huggingface/transformers, and
a server-side OpenAI Whisper fallback gated on KANDEV_VOICE_OPENAI_API_KEY.
User-configurable from a new /settings/voice-mode page:
- Master on/off toggle (defaults to on; opt-out, not opt-in)
- Engine preference (auto / Web Speech / Whisper Web / Whisper Server)
- Language hint (auto-detect or BCP-47 pinned)
- Activation mode (click-to-toggle or hold-to-talk)
- Auto-send after transcription
- Whisper Web model size (tiny / base / small)
- Editable Cmd+Shift+M keyboard shortcut
Voice settings live under user_settings.voice_mode as a nested JSON object —
no schema migration. Setting changes broadcast via the existing
user.settings.updated WS event so any open chat tab updates live.
The mic button is rendered on the right of the composer alongside the send
button (round primary fill; destructive fill with pulse ring while recording).
On browsers without a usable engine (e.g. iOS Chrome over HTTP) the button
stays visible as a muted greyed icon — tapping it surfaces a toast with the
reason (typically a missing secure context).
Whisper Web runs in a Web Worker with fp16 decoder weights (avoids the
MatMulNBits scale-missing crash that q4/q8 hit on WASM). Models are pulled
from the onnx-community/* mirror and cached by transformers.js in the Cache
Storage on first use.
* fix: address voice mode PR review feedback and E2E selector clash
Bot-review fixes from #1159:
- Map http.MaxBytesError from c.FormFile to 413 instead of bare 400.
- Strip BCP-47 region suffix before passing language hint to Whisper Web.
- Recover from worker crash: terminate + null worker + reset ready flag so
next init/transcribe creates a fresh worker instead of hanging.
- Read live store state via storeApi.getState() in voice-mode settings
savers to avoid stale-closure clobbering concurrent settings updates.
- Detach Web Speech callbacks before abort() so trailing onend/onerror
events don't mutate hook state after cancel().
- Spread caller init before method/body in voice-api fetch so a caller-
supplied init can't override the multipart POST.
- Replace t.Fatalf with t.Errorf+return inside test HTTP handler goroutine
(FailNow from non-test goroutine is unsafe).
- Add Enabled/AutoSend zeroing assertions in partial-update test so the
bool-overwrite-on-PATCH behavior is locked in explicitly.
E2E fix:
- toolbar-overflow spec used button.rounded-full which now matches both
the send and voice buttons (strict-mode violation). Switched to the
submit-message-button testid for an unambiguous locator.
* fix: scope voice settings rollback + whisper worker error to changed keys
Round 2 of PR review feedback on #1159:
- voice-mode settings rollback now restores only the keys patched by the
failing request (not the whole snapshot), preserving concurrent edits
to unrelated voiceMode fields.
- Shortcut rollback diffs previous vs next and reverts only the changed
keys (restore prior value or delete if absent), preserving other
concurrent shortcut edits.
- Whisper worker error handler captures the worker reference at attach
time and only clears refs when this.worker still matches it — a stale
error from a previously replaced worker no longer nukes the active one.
- Update stale whisper-web-models JSDoc that still pointed at Xenova
mirrors.
* fix: voice mode progress scale, hold-mode race, race-aware rollback
Round 3 of PR review feedback on #1159:
- Normalise Whisper progress to 0–1 in ensureWhisperClient (transformers.js
emits 0–100, the button's display + the `ready: 1` convention expect
0–1 — fixes the 5000% mid-download display).
- Claim driverRef synchronously at the top of finishCapture so concurrent
pointerup + pointerleave invocations early-return instead of racing and
clobbering a freshly-started recording's ref.
- Race-aware rollback: voice-mode and shortcut catch branches now skip
reverting a key when latest[key] !== next[key]. A failed earlier request
no longer overwrites a later successful save to the same key.
- Update the Worker wire-protocol comment to show onnx-community/whisper-base
instead of the stale Xenova/* example.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* fix(backend): make repository setup script failures non-fatal
A failing repository setup script aborted environment preparation and
stopped the agent ("Environment setup failed"), and Resume / Start-fresh
couldn't recover because each relaunch re-ran the same failing script.
runWorktreeSetupScript (run inside worktree creation) treated a non-zero
exit as fatal: it deleted the worktree and returned an error, which
cascaded to EnvPrepareResult{Success: false} and aborted the launch. This
contradicted the already-non-fatal task/profile-level setup script path.
The repo-level script is now non-fatal too: on failure it logs a warning,
records SetupScriptWarning on the worktree, keeps the worktree, and lets
the launch proceed. The env preparer surfaces it on the "Create worktree"
step so the panel shows completed_with_warnings instead of failed. The
full script output still streams to the script_execution chat message.
Adds backend regression tests and an e2e test (verified red before / green
after) asserting the agent still launches when the repo setup script fails.
* fix(backend): preserve base-branch warning when setup script also fails
Addresses review feedback: the post-Create block surfacing SetupScriptWarning
on the "Create worktree" step unconditionally overwrote BaseBranchFallbackWarning
when both occurred, silently dropping the "wrong branch" notice. Append instead
of replace so both survive.
Also updates TestWorktreePreparer_SetupScriptFailure_KeepsCreateWorktreeStepCompleted
to the non-fatal contract: a failed setup script now yields a successful prepare
with a warning on the step, not a failed prepare.
* refactor(web): extract measureCaretRect into shared util
Move the textarea caret-position measurement helper out of
rich-text-input.tsx into apps/web/lib/utils/caret-position.ts so the
task-create dialog's autocomplete wiring can reuse it without depending
on the chat-only RichTextInput component.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): allow custom prompts in task creation input
Add an @-mention autocomplete to the task-create dialog's prompt
textarea so users can quickly insert one of their saved custom prompts
(built-in + user-created) by typing @<name>.
- Extend useInlineMention with a promptInsertMode option. The default
"context" mode preserves chat behavior (attach the prompt to the
context store). The new "inline" mode replaces the @query text with
the prompt's full content, requiring no backend changes -- the
resolved text just becomes part of the task description on submit.
- Wire the hook into TaskFormInputs via a small useTaskCreatePromptMention
helper that adapts the existing textarea ref to the RichTextInputHandle
surface the hook expects (focus / selection / caret-rect / value).
- Reuse the existing MentionMenu popup component for parity with chat.
- Cover the new branches with unit tests for makePromptItem /
detectMentionTrigger / filterItems, and an E2E spec exercising
the menu open / inline-insertion / Enter-doesn't-submit /
trigger-inside-word paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): make mention popup clickable inside Radix Dialog
Radix Dialog sets pointer-events: none on document.body to enforce
modal isolation. The mention/slash menu portals to document.body and
inherits the disabled pointer events when opened from inside a Dialog
(the task-create flow). The popup was visible but unclickable: keyboard
select worked, mouse clicks were swallowed by the Dialog body.
Restore pointerEvents: auto on the popup container so it stays
clickable when invoked from within a modal. Bump z-index to 60 so the
popup also wins the visual stacking race against the Dialog (overlay +
content are both z-50). Chat continues to work unchanged since z-60 and
pointer-events: auto are no-ops outside a modal.
Add an adversarial QA spec covering mouse-click select, ArrowDown +
Enter on the 2nd item, Escape close, multi-line content auto-grow,
backspace-over-@ close, space-after-@ close, no-arrow-cursor-shift,
and end-to-end submit. The mouse-click case is the one this fix
unblocks; the rest pin behavior for future regressions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): address PR review feedback
- caret-position.ts: wrap DOM measurement in try/finally so the mirror
element is always removed even if getBoundingClientRect throws
- useTextareaHandlers: destructure stable fn refs from mention before
useCallback so deps are specific properties, not the whole object —
avoids defeating memoization on every keystroke
- QA spec: consolidate inline cleanupPrompts calls into a shared
test.afterEach so cleanup always runs even when a test throws; update
submit test comment to accurately reflect what is asserted
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): tighten ArrowDown selection assertion in QA E2E spec
Equal filter scores sort by insertion order (stable), so qa-arr-1 lands
at index 0 and qa-arr-2 at index 1. After one ArrowDown from the default
the selected item is "SECOND" — assert that directly instead of accepting
either value, matching the test title "selects the second prompt".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(backend): inherit parent workspace for subtasks on all start paths
Subtasks with workspace_mode inherit_parent were provisioning a fresh
worktree and tasks folder instead of reusing the parent's. Two causes:
- propagateInheritedEnvironment ran only in PrepareTaskSession, so the
direct start path (startTask, used by MCP auto-start) never bound the
child session to the parent's TaskEnvironment. Moved it into
prepareSessionForStart so every launch entry point inherits.
- The service CreateTask path did not inherit the parent's repositories
when none were supplied (the UI omits them expecting inheritance), so
the child was repo-less and could not establish/reuse a worktree. Now
mirrors the MCP path: inherit RepositoryID + BaseBranch, drop
CheckoutBranch.
* fix(web): hide executor profile for inherit-parent subtasks
When a subtask inherits the parent's materialized workspace, its
executor is inherited too, so picking an executor profile is
meaningless. Hide the selector under inherit_parent and omit
executor_profile_id from the create payload.
* fix(backend): fail fast when subtask parent-repo inheritance lookup errors
Return the error from inheritParentRepositories instead of swallowing it:
a subtask silently created with no repositories would reintroduce the
fresh-worktree bug at launch. Also clarified that inheritance applies to
any repo-less subtask, matching the MCP path.