From 01a20dcf70c296b0d279e57195fd4a7ec87ea779 Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 22 Jul 2026 09:15:45 +0900 Subject: [PATCH] feat(chat): honor app-server direct input capability --- docs/design.md | 2 +- src/app-server/protocol/thread.ts | 1 + src/domain/threads/model.ts | 5 +++ .../application/panel-operation-policy.ts | 13 +++++++ .../chat/application/state/root-reducer.ts | 3 ++ .../turns/composer-submit-actions.ts | 8 ++-- .../chat/panel/composer-controller.ts | 3 +- src/features/chat/panel/shell-selectors.ts | 2 + .../panel/surface/composer-projection.tsx | 9 +++-- src/features/chat/ui/composer.tsx | 23 ++++++++--- .../legacy-panel-user-message.test.ts | 1 + tests/app-server/query-cache.test.ts | 1 + tests/app-server/threads.test.ts | 18 +++++++++ tests/domain/threads/archive-markdown.test.ts | 1 + tests/domain/threads/deep-link.test.ts | 1 + tests/domain/threads/reference.test.ts | 1 + tests/domain/threads/search.test.ts | 1 + tests/domain/threads/threads.test.ts | 1 + .../chat/app-server/inbound/handler.test.ts | 1 + .../thread-reference-resolver.test.ts | 1 + .../panel-operation-policy.test.ts | 37 +++++++++++++++--- .../turns/composer-submit-actions.test.ts | 25 ++++++++++++ .../turns/turn-submission-actions.test.ts | 39 ++++++++++++++++++- .../chat/panel/composer-controller.test.ts | 3 +- tests/features/chat/panel/shell.test.tsx | 1 + .../chat/panel/surface/projections.test.ts | 23 +++++++++++ .../chat/panel/toolbar-archive-state.test.tsx | 1 + tests/features/chat/support/state.ts | 1 + tests/features/chat/ui/composer.test.ts | 32 +++++++++++++++ tests/support/plugin-fixtures.ts | 1 + 30 files changed, 237 insertions(+), 22 deletions(-) diff --git a/docs/design.md b/docs/design.md index d891de3b..5f6f1a65 100644 --- a/docs/design.md +++ b/docs/design.md @@ -72,7 +72,7 @@ Foreground reveal and focus are the narrow workspace-wide exception: the latest Do not use explanatory copy as the default remedy for an unclear or incoherent interface. Before adding instructions, warnings, status text, validation messages, confirmations, or diagnostics, determine why the interface does not communicate the necessary behavior through its structure and state. Prefer improving information hierarchy, labels, defaults, available choices, affordances, validation timing, progress indication, and feedback. Add text only when irreducible information remains and it materially helps the user decide, act, or recover; do not expose implementation concepts merely because they explain the current behavior. -Subagent threads opened from agent activity remain persistent and restorable but stay outside ordinary thread history. Treat their panels as read-only conversation surfaces while preserving their parent and agent provenance for future specialized behavior. +Subagent threads opened from agent activity remain persistent and restorable but stay outside ordinary thread history. Use the app-server direct-input capability when available, falling back to read-only subagent panels for older servers, while preserving parent and agent provenance for other specialized behavior. Mode-derived restrictions for the active panel thread must remain consistent across actions and UI. Keep connection state, turn busy state, and operations targeting another listed thread in their owning workflows. diff --git a/src/app-server/protocol/thread.ts b/src/app-server/protocol/thread.ts index aa491b5a..f31c1ded 100644 --- a/src/app-server/protocol/thread.ts +++ b/src/app-server/protocol/thread.ts @@ -21,6 +21,7 @@ export function threadFromThreadRecord(thread: ThreadRecord, options: { archived archived: options.archived ?? false, createdAt: finiteTimestamp(thread.createdAt), updatedAt: finiteTimestamp(thread.updatedAt), + canAcceptDirectInput: typeof thread.canAcceptDirectInput === "boolean" ? thread.canAcceptDirectInput : null, provenance: threadProvenance(thread), ...(hasRecencyAt ? { recencyAt: typeof recencyAt === "number" && Number.isFinite(recencyAt) ? recencyAt : null } : {}), }; diff --git a/src/domain/threads/model.ts b/src/domain/threads/model.ts index c07acd63..c6d10ffc 100644 --- a/src/domain/threads/model.ts +++ b/src/domain/threads/model.ts @@ -6,6 +6,11 @@ export interface Thread { readonly createdAt: number; readonly updatedAt: number; readonly recencyAt?: number | null; + /** + * Whether the loaded app-server thread accepts direct turn input. + * `null` means the capability is unavailable, so panel mode policy decides. + */ + readonly canAcceptDirectInput?: boolean | null; readonly provenance: ThreadProvenance; } diff --git a/src/features/chat/application/panel-operation-policy.ts b/src/features/chat/application/panel-operation-policy.ts index 544a008a..eb3e0253 100644 --- a/src/features/chat/application/panel-operation-policy.ts +++ b/src/features/chat/application/panel-operation-policy.ts @@ -30,6 +30,7 @@ type ActivePanelThreadFacts = | { readonly phase: "active"; readonly lifetime: "persistent" | "ephemeral"; + readonly canAcceptDirectInput: boolean | null; readonly provenance: "interactive" | "subagent" | null; }; @@ -49,6 +50,13 @@ function activePanelOperationDecisionForFacts( } if (facts.phase === "empty") return ALLOWED; + if (operation === "submit") { + if (facts.canAcceptDirectInput !== null) return facts.canAcceptDirectInput ? ALLOWED : directInputBlocked(); + if (facts.provenance === "subagent") return agentThreadBlocked(operation); + if (facts.lifetime === "ephemeral") return sideChatDecision(operation); + return ALLOWED; + } + // Keep the stricter interpretation if a malformed or future state contains // both mode facts. This makes mode restrictions monotonically safe. if (facts.provenance === "subagent" && facts.lifetime === "ephemeral") { @@ -68,10 +76,15 @@ function activePanelThreadFacts(state: ChatState): ActivePanelThreadFacts { return { phase: "active", lifetime: panelThread.thread.lifetime?.kind ?? "persistent", + canAcceptDirectInput: panelThread.thread.canAcceptDirectInput, provenance: panelThread.thread.provenance?.kind ?? null, }; } +function directInputBlocked(): ActivePanelOperationDecision { + return { kind: "blocked", message: "This thread cannot accept messages." }; +} + function agentThreadBlocked(operation: ActivePanelOperation): ActivePanelOperationDecision { switch (operation) { case "submit": diff --git a/src/features/chat/application/state/root-reducer.ts b/src/features/chat/application/state/root-reducer.ts index b4f8eb58..975ba4fb 100644 --- a/src/features/chat/application/state/root-reducer.ts +++ b/src/features/chat/application/state/root-reducer.ts @@ -123,6 +123,7 @@ export interface ChatActiveThreadState { readonly goal: ThreadGoal | null; readonly tokenUsage: ThreadTokenUsage | null; readonly lifetime: ActiveThreadLifetime | null; + readonly canAcceptDirectInput: boolean | null; readonly provenance: Thread["provenance"] | null; } @@ -463,6 +464,7 @@ function reduceActiveThreadResumedTransition(state: ChatState, action: ActiveThr goal: null, tokenUsage: null, lifetime: action.lifetime ?? { kind: "persistent" }, + canAcceptDirectInput: action.thread.canAcceptDirectInput ?? null, provenance: action.thread.provenance, }, }, @@ -900,6 +902,7 @@ function createActiveThreadState(id: string): ChatActiveThreadState { goal: null, tokenUsage: null, lifetime: null, + canAcceptDirectInput: null, provenance: null, }; } diff --git a/src/features/chat/application/turns/composer-submit-actions.ts b/src/features/chat/application/turns/composer-submit-actions.ts index 0d0756f1..3f16f31c 100644 --- a/src/features/chat/application/turns/composer-submit-actions.ts +++ b/src/features/chat/application/turns/composer-submit-actions.ts @@ -77,12 +77,12 @@ export async function submitComposer(host: ComposerSubmitActionsHost): Promise 0 ? composerSuggestionOptionId(viewId, normalizedSelectedSuggestionIndex) : undefined; @@ -165,7 +175,7 @@ export function ComposerShell({