fa7d88bee6
Co-authored-by: Kandev Agent <agent@kandev.dev>
9.0 KiB
9.0 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/plugins/voice-extraction-host.md | 2026-08-11 | complete |
Implementation Plan: Voice Plugin Host Prerequisites
Overview
Add host-owned composer adapters to every Voice Mode surface, then extend existing webhooks with per-declaration access and request-size policies. Publish the contract through frontend types, manifest validation, the frozen plugin API, and public authoring documentation. Keep core Voice Mode installed throughout; a separate plugin repository will consume these APIs and only a later parity task may remove core code.
Contract Decisions
- Composer state stays local to each mounted surface. Slot props expose current disabled/submittable state plus insertion, focus, and native submit without exposing draft contents or private refs.
chat-input-actionsgrows compatible props and covers task chat plus Quick Chat. Creation forms get explicittask-create-input-actionsandnew-session-input-actionsslots because their identifiers, validation, and submit lifecycles differ.- Plugin UI continues to use
host.api.fetch("webhooks/<key>")and the existingHandleWebhookRPC.webhooks[].accessselects public or authenticated invocation andmax_body_bytessets a bounded per-key limit up to 16 MiB; old manifests retain public/4 MiB defaults. - Plugin localization is a separate prerequisite. It must be resolved before core Voice Mode is removed, but this package adds no host i18n API.
Frontend Composer Architecture
- Add public types in
apps/web/lib/plugins/types.tsand mirror them indocs/plans/plugins/PLUGIN-API.md. Implement adapters guarded by the mounted composer generation; normal React props update disabled/submittable presentation without draft state entering Zustand. - Extend
TipTapInputHandleonly where required for the host adapter. Reuse its selection-awareinsertText, but centralize the Voice spacing rule so textarea and TipTap adapters behave identically. - Adapt
useChatInputState/ChatInputAreato expose nativehandleSubmitand the same disabled gate toChatInputPluginActions. Do not calluseMessageHandleror constructChatSubmitPayloadfrom plugin code. Quick Chat inherits the adapter through its existingChatInputAreacomposition. - Extend
TaskFormInputsHandlewith guarded focus/insertion operations or provide a sibling adapter arounduseDescriptionInput. Task creation invokes its existing form submit handler; new-session creation invokes its existinghandleSubmit, including profile, upload, and busy-state gates. - Keep slot controls in the existing toolbars on desktop. On native mobile, use the existing composer toolbar/overflow composition and 44px touch targets; do not introduce a separate drawer solely for plugin actions.
Mobile Design Contract
- Outcome and entry points: Voice plugin actions remain beside native composer controls in task chat and inside the prompt toolbar for task/new-session creation; Quick Chat uses its existing input toolbar. All actions are reachable by touch.
- Exemplars: task chat reuses
chat-input-toolbar-mobile.tsxandmobile-voice-mode.spec.ts; new session reuses the full-height dialog inmobile-new-session-dialog.spec.ts; task creation reuses the existing responsive Task Create dialog. - Hierarchy and action: the plugin action is secondary to the native submit action and never hides submit, model/profile selection, or cancellation. Recording/transcription state belongs to the plugin control; the native composer remains the single draft and submit owner.
- Surface rationale: frequent, one-tap input augmentation belongs inline in the existing toolbar, not behind navigation or a new drawer. Existing dialog and composer scroll owners remain unchanged; overlays and toolbars keep safe-area clearance and no document horizontal overflow.
- Shared behavior: capability state and native adapters are shared across presentations. Only the existing toolbar placement differs. Pixel 5 Playwright proves insertion plus native submission in task chat, task creation, and new-session creation; Quick Chat mobile coverage is included if its shipped mobile entry exposes the full toolbar.
Backend Authenticated Webhook Architecture
- Extend manifest
WebhookwithAccessandMaxBodyBytes. Validateaccessaspublic|authenticated, default it topublic, default the limit to 4 MiB, reject non-positive or16 MiB values, and preserve those values through install/registry serialization.
- Keep the existing webhook URL and
HandleWebhookRPC. In the handler, resolve the declared key before reading the body, apply its effective limit, and require normal Kandev identity plus accepted Origin for authenticated cookie requests. Authentication-disabled mode uses the synthetic user. - Propagate
Request.Context()cancellation throughService.InvokeWebhook; bound and sanitize the response as today. Logs may add access mode and byte count but never request content or secrets. - Keep existing public webhooks byte-for-byte compatible: no authentication requirement and a 4 MiB default. The Voice plugin stores its OpenAI key through existing secret config fields; no host key storage or new secret API is introduced.
Localization And Compatibility
- Add optional manifest fields without changing plugin
api_version: 1. Document that consumers must declare the first supportingmin_kandev_version; older hosts fail installation instead of falling back to webhooks. - Update
docs/specs/plugins/spec.md,docs/plans/plugins/PLUGIN-API.md,docs/public/plugins-authoring.md, anddocs/public/plugins-manifest.mdin the same contract task.
Test Strategy
- Frontend unit/component tests prove selection replacement, spacing, focus, live state props, stale-adapter failure, native submit delegation, blocked submission, cleanup, Quick Chat props, and desktop/mobile slot placement across all composer families.
- Backend manifest tests prove defaults, validation, and compatibility. Handler/service tests prove authenticated/public access, Origin enforcement, 10 MiB success, >16 MiB rejection, cancellation, disabled/runtime failure, and unchanged legacy webhook behavior.
- Desktop and mobile Playwright install or inject a fixture plugin action, insert a distinctive string at a non-terminal selection, invoke native submit, and assert the resulting task/message/session. A backend-integrated E2E action uploads >10 MiB only where the container/runtime fixture is available; focused Go integration tests remain the deterministic size/security proof.
Implementation Waves And Tasks
Wave 1:
Wave 2:
- Task 03: Adapt chat and Quick Chat composers (depends on Task 01)
- Task 04: Adapt creation composers (depends on Task 01)
- Task 05: Enforce authenticated webhooks (depends on Task 02)
Wave 3:
- Task 06: Document and verify extraction prerequisites (depends on Tasks 03-05)
Tasks in the same wave are parallel-safe only where their owned files remain disjoint. Execution stays sequential unless the user explicitly authorizes subagents.
Risks
- A capability that snapshots React state can auto-submit after a gate changes;
submit()must revalidate the native gate at call time. - React Strict Mode or a task/session switch can leave stale plugin closures alive; generation guards must make them fail closed rather than target the newly mounted editor.
- Conditional authentication inside a route currently treated as public must fail closed per declared key; direct handler and middleware-boundary tests are required.
- Raising gRPC message limits globally would unnecessarily affect other plugin traffic; configure only the existing webhook call path sufficiently for the 16 MiB host ceiling.
- Core Voice removal in this package would make rollback and parity validation unsafe; it is explicitly deferred to the plugin implementation/extraction task.
Verification Commands
make -C apps/backend test
make -C apps/backend lint
cd apps && pnpm install --frozen-lockfile
cd apps && pnpm --filter @kandev/web test -- --run lib/plugins components/task/chat components/task-create-dialog-selectors.test.tsx components/task/new-session-dialog.test.tsx
cd apps/web && pnpm run typecheck
cd apps/web && pnpm run i18n:check && pnpm run i18n:ratchet
make build-web && make build-backend
cd apps/web && pnpm e2e:raw --project=chrome e2e/tests/plugins/composer-actions.spec.ts e2e/tests/chat/quick-chat.spec.ts e2e/tests/session/new-session-dialog.spec.ts
cd apps/web && pnpm e2e:raw --project=mobile-chrome e2e/tests/plugins/mobile-composer-actions.spec.ts e2e/tests/session/mobile-new-session-dialog.spec.ts
node --test scripts/validate-public-docs.test.mjs