* docs: add plugin system guide, authoring tutorial, and manifest reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update plugin docs for host data API, config, and nav additions
Cherry-picked plugin docs (d853b66b8) were written before four follow-up
PRs merged: typed capability-gated Host data API (#1753, ADR 0043),
richer frontend SDK (#1759), per-plugin settings pages (#1761), and
sidebar Integrations nav items (#1767). Re-verify against the merged
code and bring the docs current:
- plugins.md: per-plugin settings/config page, expanded Host RPC list,
updated capability vocabulary (api_read/api_write), sidebar nav note.
- plugins-authoring.md: GetConfig/GetSecret/SetSecret/DeleteSecret and
the six Host data readers, registerRoute options/PluginPageChrome,
NavItem icon/integrations section, host.api.baseUrl/navigate, and the
full host.ui component list.
- plugins-manifest.md: corrected api_read/api_write semantics (now
enforced, not reserved), config_schema validation/secret-masking
behavior, and new Host data API / config-schema sections.
- configuration.md, feature-status.md: document the KANDEV_FEATURES_PLUGINS
flag alongside the existing Office flag.
- coverage.json: new plugin-system coverage area so the public-docs
validator accounts for the three new pages.
* docs: fix plugin config route, signing claim, and data-dir layout
Address automated review findings on PR #1774:
- plugins-manifest.md: PATCH route for config is /api/plugins/{id}, not
/api/plugins/{id}/config (only GET uses the /config suffix).
- plugins-authoring.md: host.store is not read-only — setState is
genuinely exposed and writable; correct the misleading inline comment.
- plugins.md: signature verification is not currently wired up in
production (pkgtar.VerifySignature is nil, never assigned outside
tests), so every package installs and is reported as unsigned today —
correct the 'kandev verifies it' claim in both the install pipeline
step and the signed/unsigned section.
- plugins.md: KANDEV_PLUGIN_DATA_DIR is <id>/data, a sibling of the
<version>/ directory (shared across versions), not nested inside it —
fix the on-disk layout diagram.
* docs: mark plugin docs as experimental
The plugin system is feature-flagged and off by default in production
(KANDEV_FEATURES_PLUGINS). Mark all three plugin pages with
status: experimental frontmatter, matching the k8s.md convention for a
page that is wholly about an experimental feature.
* docs: add plugin UI screenshots and a capture spec
Embed four operator-facing screenshots in the plugin docs (the Plugins
list, the install dialog, a per-plugin settings page with its manifest
card, and a native plugin route). Captured from an isolated e2e backend
with the kandev-plugin-hello reference example installed.
Add e2e/tests/plugins/plugins-docs-screenshots.spec.ts to regenerate them:
it reuses the plugins e2e harness (isolated backend, plugins flag on) and
is skipped unless CAPTURE_DOCS_MEDIA=1, so it adds no CI cost. Defaults to
the repo's own e2e fixture package; override DOCS_PLUGIN_* to point at the
polished hello example (as the committed captures were taken).
* docs: fix plugin screenshot paths for the landing publisher
The Cloudflare preview build failed because markdown images under
docs/public/media/ compile to build-time module imports that the docs
site can't resolve — media/ is reserved for <DocsVideo> assets. The
landing publisher only rewrites and copies images referenced under
screenshots/ (README convention). Move the four plugin captures to
docs/screenshots/plugin-*.png and reference them as ../screenshots/*.png,
and update the capture spec to write there.
* docs: convert plugin how-it-works diagram to mermaid
The hand-drawn ASCII pipeline rendered with broken alignment and stray
syntax highlighting on the docs site. Replace it with a mermaid flowchart
(same convention as architecture.md), which renders as a clean SVG.
* docs: give plugins its own nav section
* docs: clarify plugin tools are declared/listed, not agent-invocable yet
Plugin tools[] are validated and exposed via GET /api/plugins/tools, and
the gRPC InvokeTool transport is implemented and tested, but nothing in
production calls it — no agent, MCP bridge, or HTTP route. The docs
overstated this as agent-invocable. Correct the Plugin interface comment,
the manifest tools[] rows, the how-it-works diagram, and the example
plugin description, and add a note in both the authoring and manifest
pages that InvokeTool stays unused until the agent-tool wiring ships.
* docs: clarify webhook relay in how-it-works diagram
The diagram collapsed two hops into one node, reading as if the plugin
receives the external HTTP POST directly. Split it: an external caller
POSTs to kandev's webhook route, which relays the request to the plugin
over the gRPC HandleWebhook method (the plugin serves no HTTP itself).
Also label the DeliverEvent/InvokeTool/HandleWebhook edges by their real
proto method names.
* docs: note the plugin backend is any go-plugin gRPC executable, Go SDK only
* docs: remove plugin tools from the docs
Plugin tools[] overlap with MCP (kandev's established agent-tool surface)
and the invocation path was never wired to agents. Rather than document a
half-feature that implies plugins are an alternate MCP, drop it entirely:
the Plugin interface (InvokeTool), the manifest tools[] block and field
rows, the how-it-works diagram branch, and the example-plugin mention.
Removal from the backend code is tracked separately.
* docs: replace how-it-works mermaid with a designed diagram image
The mermaid flowchart rendered poorly. Replace it with a hand-authored
HTML/CSS architecture figure (install pipeline + the runtime gRPC
interactions), rendered to docs/screenshots/plugin-architecture.png. The
diagram source lives in the docs-screenshot capture spec (skipped unless
CAPTURE_DOCS_MEDIA=1) so it regenerates deterministically at 2x.
* docs: make the how-it-works figure a real box-and-arrow diagram
The previous figure read as a list of rows. Replace it with a hand-drawn
SVG architecture diagram: the install pipeline feeds a spawned plugin
subprocess, then kandev and the plugin communicate over one gRPC
connection (DeliverEvent, HandleWebhook, and the plugin's Host API
callbacks), with the external webhook caller and the browser SPA as
edge nodes. Source lives in the capture spec's ARCH_HTML.
* docs: make the how-it-works diagram a clean monochrome flowchart
Drop the multi-color component diagram for a calm top-down flowchart
(mermaid's shape, drawn properly): install → verify → extract → spawn,
branching into event delivery and webhook relay, converging on the Host
API callbacks, with an optional native-UI step. Grayscale, no accent
colors. Source stays in the capture spec's ARCH_HTML.
* docs: dark plugin diagram + space out the two page images
- Make the how-it-works flowchart a dark rounded card (matches the docs
page in dark mode; reads as an embedded card in light). Rendered with
omitBackground so the corners are transparent.
- Move the Settings > Plugins list screenshot out of the intro (where it
sat right before the diagram) down into the "Installing a plugin"
section, so text separates the two images.
* docs: update install-dialog screenshot for aligned footer actions
Recaptured after fix(plugins): align install-dialog footer actions
(#1780) — Cancel and Install now sit together in one right-aligned
footer row instead of the Install button being stranded under the
dropzone.
* docs: account for /settings/system/storage in coverage.json
The storage settings route (added by #1699) was never recorded in the
public-docs coverage inventory — the docs validator is path-filtered to
docs/public/** so it didn't run for that PR. This docs PR is the first to
trip it. Add the route to system-health-and-maintenance.
* docs: review fixes — remove stale tools refs, tighten accuracy
Review pass against the now-merged main (tools removed in #1776):
- extending-kandev.md: drop "serves agent-invocable tools" from the plugin
overview (the feature no longer exists).
- plugins-manifest.md: the legacy endpoints block is health/events/webhooks
only (no tools path); and the `signed` field is always false today since
signature verification isn't wired.
- plugins.md: install validation covers the categories/UI-surface enums (not
a "capability vocabulary"); AutoMTLS secures the channel on all platforms,
not just Windows; and status vs signing are separate badges.
* docs: explain what Host state is (GetState/SetState)
The authoring guide listed the state RPCs but never said what 'state'
means. Add a plain-language explanation: a per-plugin, kandev-persisted
key/value store keyed by (scope, scopeID, key) holding JSON objects,
namespaced per plugin, scoped to instance/workspace/task/agent, and how it
differs from the writable data dir. Make the example a real increment.
* docs: note NavItem section:'settings' is not rendered
A NavItem with section:'settings' is accepted by the type but no sidebar
section renders it (plugin-nav-items renders main/default, the Integrations
sections render 'integrations'); annotate that settings pages use
registerSettingsRoute. Addresses repeated review feedback.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(web): improve mobile task and kanban navigation
* feat(web): refine mobile task navigation
* fix(web): address mobile navigation review feedback
* test(web): cover task title field modes
* fix(web): keep mobile board navigator open across workflows
* fix(web): address mobile workflow review feedback
* feat(web): surface Integrations section in mobile nav
Add a mobile-reachable entry point for the sidebar Integrations section.
The global AppSidebar is desktop-only (hidden md:block), so on a phone
viewport neither the first-party integration links nor plugin nav items
with section: "integrations" were reachable. Wire the pre-existing
MobileIntegrationsSection into the kanban hamburger sheet and extend it
to render plugin items (gated on the plugins flag), matching the desktop
IntegrationsSection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(web): track registered plugin ids for cleanup in mobile integrations tests
Route all plugin nav-item registrations through a tracking helper and
unregister the collected ids in a suite-scoped afterEach, so a future test
that registers a new plugin id cannot leak items into subsequent tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(web): stabilize sidebar toggle animation
Keep Dockview panes fixed while sidebar chrome animates between its open and closed widths.
* ci: align docs workflow pnpm version
Match pnpm/action-setup to the workspace packageManager pin.
* fix(web): address sidebar review feedback
Harden sidebar E2E assertions and restore the shared repository fixture after mutation.
* feat(plugins): per-plugin settings pages driven by manifest config_schema
Settings > Plugins rows now open /settings/plugins/:id — a detail page
showing the manifest plus a settings form generated from the plugin
author's config_schema. Secret fields (secret: true / format: password)
are masked on the operator API and kept on masked re-submits; a running
plugin restarts on save and reads its config (cleartext secrets included)
through the new Host GetConfig gRPC RPC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): write plugin config file owner-only (0600)
Operator config can now carry cleartext secret values (config_schema
fields marked secret), so <id>.config.yml must not be world-readable —
match the 0600 mode the record file already uses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(plugins): vault-backed plugin secrets — Get/Set/DeleteSecret RPCs and encrypted config secrets
Plugins get vault-backed secret primitives on the Host API
(GetSecret/SetSecret/DeleteSecret, capability-gated, namespaced to
plugin:<id>㊙️<key> so a plugin can only touch its own entries;
purged on uninstall). Secret config_schema fields now live in kandev's
AES-256-GCM vault too — <id>.config.yml persists only a vault reference,
and the Host GetConfig RPC resolves it back to cleartext for the plugin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(plugins): serve PR screenshots from the media/pr-1761-screenshots ref instead of the repo
The PR body now points at raw URLs pinned to an unmerged media ref, so
the binaries never land in main (PRs are squash-merged; only the branch
tip tree merges).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address review feedback on settings validation and save flow
- integer form fields reject non-integral input instead of parseInt-truncating
- enum selections keep their schema-declared types (numeric enums round-trip)
- backend enum validation normalizes int-vs-float64 before comparison
- maskSecrets masks any non-zero secret value regardless of runtime type
- save flow splits PATCH from refetch: a refetch failure no longer reports a
successful save as an error, and typed secrets are masked on screen
- optional enum fields gain a 'Not set' option so they can be cleared
- resolveConfigSecrets copies before mutating; restart-failure SetStatus
errors are logged; SDK test fake returns a non-nil empty config map
- add missing tests: usePluginConfigForm hook, getPluginConfig client,
integer/enum regression cases
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): harden secret config invariants from review
- reject non-string values on declared secret fields at validation time,
closing the path where a numeric/boolean secret bypassed the vault and
persisted cleartext in the config file
- delete removed secret fields' vault entries only AFTER the config commit
succeeds, so a failed SetConfig never leaves the still-current config
referencing a deleted vault entry
- uninstall fails closed: the plugin's vault namespace is purged before any
destructive step, and a cleanup failure aborts the uninstall (retryable)
instead of letting a reinstall inherit stale secrets
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): failure-atomic config secrets, quiesced uninstall purge, ctx propagation
- UpdateConfig is now failure-atomic for secret overwrites: storeConfigSecrets
snapshots each vault entry before writing and returns a rollback that
restores prior values (or deletes newly-created ones). A vault.Set failure
mid-loop or a failed SetConfig commit rolls back, so a request reported as
failed never changes the value a still-current config ref resolves to.
- Uninstall stops the plugin process BEFORE purging its vault namespace, so
the plugin can no longer race the cleanup with a SetSecret between ListIDs
and the deletes; the purge stays fail-closed and retryable.
- Thread the request context through Uninstall -> deletePluginSecrets instead
of context.Background(), per the pass-context-through-chains convention.
- Consolidate the 'secret not found:' prefix check into isSecretNotFound
(host.go + service rollback share one definition).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): detach rollback context, fail closed without a vault, reconcile uninstall status
- vault rollback closures now run on context.WithoutCancel(ctx): a request
cancelled mid-save (browser closed) can no longer abort the rollback and
leave the vault holding new values while the unchanged config still points
at them (Greptile P1).
- storeConfigSecrets fails closed (errSecretVaultRequired) when a plugin
declares secret config fields but no vault is wired, instead of silently
persisting the secret in cleartext.
- vaultRestoreFunc distinguishes not-found from a genuine Reveal error: a
backend read failure aborts the update before writing rather than risk a
rollback that deletes a real secret; rollback now returns errors, joined
into UpdateConfig's failure so an inconsistent vault is surfaced, not just
logged.
- Uninstall reconciles lifecycle status when a secret-purge failure aborts
after the process was stopped: a previously-running plugin transitions to
error and notifies observers instead of being reported active with no
process running.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): detach removed-secret cleanup ctx and clear stale save dirty flag
- cleanupRemovedConfigSecrets now runs on context.WithoutCancel(ctx), matching
the rollback path, so a client disconnect right after the config commit can
no longer cancel the delete and orphan now-unreferenced vault entries.
- tests: assert GetConfig succeeds before treating an empty map as proof that
nothing persisted (a swallowed read error would be a false pass).
- web: on a post-save refetch failure, rebase initialValues onto the masked
snapshot so the saved form no longer reads as dirty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): fail tests on discarded GetConfig errors, drop setState-in-updater
- config_test: capture and fatal on GetConfig errors at the four remaining
positive-assertion sites (a nil from a failed read would panic on the next
map access instead of giving a clear failure), matching the earlier fix.
- web hook: compute the masked post-save snapshot from the current values and
apply it to setValues + setInitialValues directly, instead of calling
setInitialValues inside a setValues functional updater (a React anti-pattern
that runs twice under StrictMode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): skip validation of a secret field's own vault ref
- validateConfigSchema takes the plugin id and skips a secret field whose
value is that field's config vault ref (an internal storage marker,
already validated when first set). Without this, a field declaring both
secret: true and enum: [...] 400s on every save that keeps the mask,
because the enum check saw the vault ref instead of a user value. Freshly
entered secret values are still validated; a foreign plugin's ref is not
skipped. Covered by unit + service-level regression tests.
- surface mid-loop vault rollback failures in storeConfigSecrets via a
warning instead of discarding them (double-fault observability).
- web: integer inputs get step=1 so browsers flag non-integral input in
place (serializeConfigValues remains the backstop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(plugins): split config secret tests into config_secrets_test.go
config_test.go had grown past the 800-line backend file-length limit
(revive), failing CI lint. Move the vault-backed config and plugin-scoped
secret tests (plus their fake-vault/store helpers) into a sibling file;
no test logic changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(web): add quick chat entry points on mobile
Quick chat was unreachable on touch devices: every trigger was
desktop-only (keyboard shortcut, command palette, app sidebar) and the
full-screen dialog had no touch dismissal.
- add quick chat button to the mobile kanban/tasks header
- add quick chat button to the tablet header (640-767px has no sidebar)
- add chat button to the mobile session task-switcher sheet
- add a mobile-only close control to the quick chat tab bar
- cover the touch entry points with a mobile Playwright spec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(web): address review feedback on mobile quick chat spec
- reuse the shared assertNoDocumentHorizontalOverflow layout helper in
both quick chat mobile specs instead of local duplicates
- assert the task switcher sheet is dismissed after opening quick chat
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add plugin system with grpc runtime and native ui plugins
- go plugin backends ship as release tarballs (per-platform binaries, checksums) that
kandev installs from url/upload or filesystem sideload+sync, then spawns and
supervises via hashicorp/go-plugin (unix socket / loopback tcp+automtls)
- strict typed grpc contract (kandev.plugin.v1): DeliverEvent/InvokeTool/HandleWebhook
plus a capability-gated host service (state, secrets, emit); author sdk in
pkg/pluginsdk; packaging cli in cmd/plugin-pack
- native frontend bundles: window.registerKandevPlugin + registry/host loader with
dynamic routes, nav items, slots and ws handlers; plugins settings page with
install dialog, sync button and enable/disable/uninstall
- gated behind the plugins feature flag (prod off, dev/e2e on); covered by unit and
integration tests plus playwright e2e against a real packaged fixture plugin
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): hydrate base app context on plugin routes
Plugin-owned SPA routes classify as RouteUnknown, so the boot payload shipped
only feature flags — no workspaces, workflows, or kanban context. Native plugin
UI (e.g. host.ui.TaskCreateDialog) then had an empty store. Hydrate the same
base context as the Home route for unknown SPA routes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(plugins): expose TaskCreateDialog on host.ui
Lets a native plugin open kandev's real create-task modal (prefilled via
initialValues) instead of POSTing directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): harden filesystem/URL sinks against CodeQL findings
Add sink-level guards on the plugin trust boundary: securejoin containment on
archive extraction (zip-slip), plugin-id validation before every fs_store path,
http/https scheme validation on install-from-URL, and non-interpolated ws-handler
error logging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address review findings (races, resource caps, lifecycle)
- serialize per-plugin startup to close a TOCTOU double-spawn race, and let a
plugin restart after exhausting attempts
- bound plugin startup with an explicit 30s timeout
- reject backslash paths and cap decompressed bytes by actual size in pkgtar;
only mark a package Signed when a verifier actually ran and passed
- delete plugin_state on uninstall; validate webhook keys against the manifest
and cap webhook body size; restart an active plugin when a new version installs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): sanitize plugin-id path sinks for CodeQL
Pin the plugin id to a single path segment with filepath.Base at recordPath/
configPath and pkgtar.Remove — a barrier the taint analysis recognizes.
Behaviour-identical for valid ids (already validated by safePluginID); clears
the go/path-injection alerts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): inline plugin-id validation at path sinks for CodeQL
filepath.Base does not strip '..', so it is not a path-injection barrier. Call
the recognized safePluginID / id-pattern guard inline in Delete/GetConfig/
SetConfig and pkgtar.Remove — the same barrier that already cleared the Get sink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address code-review findings (backend)
- add capability-gating tests for the Host RPC service (was untested)
- close a stop-during-start orphan race so Disable/Uninstall can't leave a
supervised subprocess behind
- on failed upgrade, remove only the new version dir (not all versions + data);
restart the prior version
- validate manifest version and reject ids ending in .config (record-file alias)
- make Deliverer.Refresh non-blocking and cancel in-flight delivery on worker stop
- clamp out-of-range webhook status, skip-and-warn corrupt records in List,
write records atomically, semver-aware sideload version pick
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address code-review findings (frontend)
- wrap plugin routes + settings routes in a shared PluginErrorBoundary so a
throwing plugin page shows a fallback instead of white-screening the SPA
- fix disable/re-enable in one session: keep the cached registration and
re-run initialize instead of relying on a cached ES-module re-import
- prefix root-relative bundle/style URLs with apiBaseUrl (split-origin/Tauri)
- surface partial-install warnings via toast.warning instead of a bare success
- dedupe/remove injected style links on unload; gate PluginNavItems on the flag
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): serialize per-plugin lifecycle + guard config writes
- add a per-plugin keyed lock in Service so concurrent Enable/Disable/Install/
Uninstall/UpdateConfig for one id are serialized (no status-machine race)
- take s.mu in SetConfig/GetConfig and write config atomically
- bound each plugin initialize() with a timeout so one hung bundle can't stall
the rest of the boot load
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(plugins): split service_test.go to satisfy file-length lint
Move the install/URL test group into service_install_test.go; no logic change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: build and ship the e2e fixture plugin package to E2E shards
The plugin E2E specs' global-setup requires apps/backend/.build/kandev-plugin-e2e-1.0.0.tar.gz.
Build it in the E2E build job, upload it as an artifact, and download it in both
the regular and container shard jobs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: choose worktree when opening IDE on multi-repo tasks
On multi-repo tasks the open-in-IDE button always opened the first
worktree. The editors menu now shows a worktree picker (repo + branch)
when a session has multiple worktrees, and each editor in the dropdown
expands into a per-worktree submenu.
- open-editor/open-folder accept an optional worktree_id; empty keeps
the first-worktree fallback so existing clients are unaffected
- session DTOs now serialize the full worktrees list instead of only
the flattened first-worktree fields
- useSessionWorktrees unions API worktrees with the WS-populated list
- new Playwright spec covers the picker on a real multi-repo session
* fix: address review feedback on worktree picker
- fall back on empty-string worktree fields, not only null/undefined
- bind optional open-folder body via EOF check instead of ContentLength
so chunked requests keep their worktree_id
- wrap disabled editor buttons in a focusable span so tooltips work
- add hook test asserting worktreeId is forwarded as worktree_id
* fix: map editor service errors to HTTP status, drop e2e sleep
- open-editor/open-folder return 404/400/409 for known service errors
(unknown worktree or editor, invalid config, unavailable editor)
instead of a blanket 500
- replace the picker spec's 750ms sleep with an explicit composer click
so the autofocus race is deterministic
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat: rename task inline from the top bar via double-click
Double-clicking the task title in the desktop top bar swaps it for an
inline input; Enter commits the rename through the existing task update
path, Escape or blur cancels. Archived tasks stay read-only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: fix vi.fn generics in agent usage test for vitest 4
vi.fn dropped the two-type-argument form in the Vitest 4 upgrade; the
old signature made mockResolvedValue infer never and fail typecheck.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: ignore Enter during IME composition in topbar rename input
Review follow-up: without the guard, the Enter that confirms an IME
candidate committed the rename mid-typing. Mirrors the quick-chat tab
rename input and the dialog default-action convention.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: harden topbar rename commit path against IME 229 and mid-edit archive
Independent-review follow-up:
- also treat keyCode 229 as IME composition, matching the documented
convention in dialog-default-action (some IMEs report the accepting
Enter after isComposing flips false)
- re-check isArchived on Enter so a task archived while the inline
editor is open cannot be renamed from it
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: make topbar rename title keyboard-operable
Addresses PR #1702 review: the renameable title exposed role="link" with
aria-disabled="false" but only a double-click handler. Add tabIndex, Enter
to open the rename input, and focus restoration on keyboard exits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>