You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/09/01 15:03:59 UTC
(maka) 02/07: fix(desktop): keep form responses behind feature boundaries
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch feat/4364-desktop-form-interactions
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 7ae4c92bbefb42ab97e2627312ee41123280d889
Author: me2seeks <me...@gmail.com>
AuthorDate: Tue Sep 1 02:26:43 2026 +0800
fix(desktop): keep form responses behind feature boundaries
Route main-surface form answers through the existing Desktop platform adapter, expose test-only companion helpers from the Workbar testing entry, and keep legacy AppShell debt monotonic while sharing response settlement behavior.
Add focused coverage for answer acceptance and failure retention.
Part of #4364.
---
apps/desktop/renderer-architecture.json | 4 +-
.../__tests__/app-shell-chat-actions-fixture.ts | 1 +
.../__tests__/app-shell-form-interaction.test.ts | 86 ++++++++++++++++++++++
.../chat-composer-region-draft-handoff.test.ts | 1 -
.../__tests__/quote-companion-interactions.test.ts | 2 +-
.../desktop/src/renderer/app-shell-chat-actions.ts | 84 ++++++---------------
.../src/renderer/app-shell-session-events.ts | 7 +-
apps/desktop/src/renderer/app-shell.tsx | 3 +-
apps/desktop/src/renderer/chat-composer-region.tsx | 6 +-
.../workbar/controller/use-workbar-controller.ts | 18 ++++-
10 files changed, 136 insertions(+), 76 deletions(-)
diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index db13adebe6..5a317e9c20 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -342,7 +342,7 @@
"nonTriviaTokens": 92
},
"src/renderer/app-shell-chat-actions.ts": {
- "importDeclarations": 26,
+ "importDeclarations": 25,
"bridgePaths": {
"window.maka.newTasks.create": 1,
"window.maka.sessions.remove": 1,
@@ -360,7 +360,7 @@
"createAppShellChatActions"
],
"dependencyPaths": {
- "../preload/bridge-contract.js": 2,
+ "../preload/bridge-contract.js": 1,
"./app-shell-copy.js": 1,
"./app-shell-session-ui-state.js": 1,
"./attachment-preflight.js": 1,
diff --git a/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts b/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts
index ad52d98382..3f40151bf2 100644
--- a/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts
+++ b/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts
@@ -118,6 +118,7 @@ export function createActionsDeps() {
transcriptRangeRef: { current: undefined },
setLiveTurnBySession: () => undefined,
setInteractionBySession: () => undefined,
+ respondToUserForm: async () => undefined,
showModelSetupToast: () => undefined,
toastApi: { error: () => undefined, info: () => undefined },
newChatModel: null,
diff --git a/apps/desktop/src/main/__tests__/app-shell-form-interaction.test.ts b/apps/desktop/src/main/__tests__/app-shell-form-interaction.test.ts
new file mode 100644
index 0000000000..2c25b9fab6
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/app-shell-form-interaction.test.ts
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { strict as assert } from 'node:assert';
+import { describe, it } from 'node:test';
+import type { InteractionQueues } from '@maka/ui';
+import { createAppShellChatActions } from '../../renderer/app-shell-chat-actions.js';
+import { createActionsDeps } from './app-shell-chat-actions-fixture.js';
+
+function pendingForm(): InteractionQueues {
+ return {
+ 'session-1': [{
+ type: 'form_request',
+ id: 'event-1',
+ turnId: 'turn-1',
+ ts: 1,
+ requestId: 'form-1',
+ toolUseId: 'tool-1',
+ message: 'Configure deployment',
+ requester: { name: 'deploy' },
+ fields: [{ kind: 'boolean', name: 'confirm', label: 'Confirm', required: true }],
+ }],
+ };
+}
+
+describe('AppShell form interaction response', () => {
+ it('retires the local prompt only after the platform accepts the answer', async () => {
+ const deps = createActionsDeps();
+ deps.activeIdRef.current = 'session-1';
+ let interactions = pendingForm();
+ let submitted: unknown;
+ const actions = createAppShellChatActions({
+ ...deps,
+ respondToUserForm: async (sessionId, response) => {
+ submitted = { sessionId, response };
+ },
+ setInteractionBySession: (update) => {
+ interactions = update(interactions);
+ },
+ });
+
+ const response = { requestId: 'form-1', action: 'accept' as const, values: { confirm: true } };
+ await actions.respondToUserForm(response);
+
+ assert.deepEqual(submitted, { sessionId: 'session-1', response });
+ assert.deepEqual(interactions['session-1'], []);
+ });
+
+ it('keeps the prompt answerable when the platform rejects the answer', async () => {
+ const deps = createActionsDeps();
+ deps.activeIdRef.current = 'session-1';
+ let interactions = pendingForm();
+ let errors = 0;
+ const actions = createAppShellChatActions({
+ ...deps,
+ respondToUserForm: async () => {
+ throw new Error('Host unavailable');
+ },
+ setInteractionBySession: (update) => {
+ interactions = update(interactions);
+ },
+ toastApi: { error: () => { errors += 1; }, info: () => undefined },
+ });
+
+ await actions.respondToUserForm({ requestId: 'form-1', action: 'cancel' });
+
+ assert.equal(interactions['session-1']?.[0]?.requestId, 'form-1');
+ assert.equal(errors, 1);
+ });
+});
diff --git a/apps/desktop/src/main/__tests__/chat-composer-region-draft-handoff.test.ts b/apps/desktop/src/main/__tests__/chat-composer-region-draft-handoff.test.ts
index 26394dad8f..2763228463 100644
--- a/apps/desktop/src/main/__tests__/chat-composer-region-draft-handoff.test.ts
+++ b/apps/desktop/src/main/__tests__/chat-composer-region-draft-handoff.test.ts
@@ -123,7 +123,6 @@ async function mountRegion(): Promise<{
respondToSandboxBoundary: () => {},
respondToClientCapability: () => {},
respondToUserQuestion: () => {},
- activeForm: undefined,
respondToUserForm: () => {},
stop: () => {},
onSend: () => {},
diff --git a/apps/desktop/src/main/__tests__/quote-companion-interactions.test.ts b/apps/desktop/src/main/__tests__/quote-companion-interactions.test.ts
index 5dfd987348..c6ec68741e 100644
--- a/apps/desktop/src/main/__tests__/quote-companion-interactions.test.ts
+++ b/apps/desktop/src/main/__tests__/quote-companion-interactions.test.ts
@@ -19,7 +19,7 @@
import { strict as assert } from 'node:assert';
import { it } from 'node:test';
-import { applyCompanionInteractionEvent } from '../../renderer/features/workbar/tools/side-chat/quote-companion-core.js';
+import { applyCompanionInteractionEvent } from '../../renderer/features/workbar/testing.js';
it('keeps companion forms pending until the Host acknowledgement arrives', () => {
let queues = applyCompanionInteractionEvent({}, 'fork-1', {
diff --git a/apps/desktop/src/renderer/app-shell-chat-actions.ts b/apps/desktop/src/renderer/app-shell-chat-actions.ts
index 0222f61caa..24b1f6cbcd 100644
--- a/apps/desktop/src/renderer/app-shell-chat-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-chat-actions.ts
@@ -19,7 +19,7 @@
import type { ChatDefaultPermissionMode } from '@maka/core/settings';
import type { CollaborationMode } from '@maka/core/collaboration';
-import type { DesktopNewTaskTarget } from '../preload/bridge-contract.js';
+import type * as DesktopBridge from '../preload/bridge-contract.js';
import type { InlineReference, QuoteRef } from '@maka/core/events';
import type { OrchestrationMode } from '@maka/core/orchestration';
import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
@@ -28,9 +28,7 @@ import type { StoredMessage } from '@maka/core/session';
import type { ThinkingLevel } from '@maka/core/model-thinking';
import type { TurnOrchestration } from '@maka/core/runtime-inputs';
import type { UiLocale } from '@maka/core/ui-locale';
-import type { DesktopSessionSummary } from '../preload/bridge-contract.js';
import type { UserQuestionResponse } from '@maka/core/user-question';
-import type { InteractionFormResponse } from '@maka/core/interaction';
import { DEFAULT_SESSION_NAME } from '@maka/core/session-name';
import {
armLiveTurn,
@@ -93,6 +91,11 @@ type PendingNewChatModel = {
} | null;
type PendingNewChatThinkingLevel = ThinkingLevel | null;
+type DesktopNewTaskTarget = DesktopBridge.DesktopNewTaskTarget;
+type DesktopSessionSummary = DesktopBridge.DesktopSessionSummary;
+type InteractionFormResponse = Parameters<
+ DesktopBridge.MakaBridge['sessions']['respondToUserForm']
+>[1];
type ToastApi = {
error(
@@ -181,6 +184,7 @@ export function createAppShellChatActions(deps: {
onInteractionChanged?: (sessionId: string) => void;
/** A boundary decision settled: the session's execution boundary may have moved. */
onExecutionBoundaryChanged?: (sessionId: string) => void;
+ respondToUserForm: DesktopBridge.MakaBridge['sessions']['respondToUserForm'];
showModelSetupToast: (
description: string,
reason?: string,
@@ -226,6 +230,7 @@ export function createAppShellChatActions(deps: {
setInteractionBySession,
onInteractionChanged,
onExecutionBoundaryChanged,
+ respondToUserForm: submitUserForm,
showModelSetupToast,
toastApi,
newChatModel,
@@ -721,67 +726,20 @@ export function createAppShellChatActions(deps: {
}
}
- async function respondToSandboxBoundary(response: SandboxBoundaryResponse) {
+ async function respondToInteraction<Response extends { requestId: string }>(
+ response: Response,
+ submit: (sessionId: string, response: Response) => Promise<void>,
+ onApplied?: (sessionId: string) => void,
+ ) {
const sessionId = activeIdRef.current;
if (!sessionId) return;
try {
- await window.maka.sessions.respondToSandboxBoundary(sessionId, response);
+ await submit(sessionId, response);
onInteractionChanged?.(sessionId);
- // #1611: the answer has been applied to the authoritative boundary, so
- // the permission label must stop describing the pre-decision one. The
- // ack event covers decisions settled on other surfaces; this covers the
- // one the user just made here, without waiting for the round trip.
- onExecutionBoundaryChanged?.(sessionId);
+ onApplied?.(sessionId);
setInteractionBySession((current) =>
dequeueInteractionByRequestId(current, sessionId, response.requestId),
);
- } catch (error) {
- // Same fire-and-forget call site as stop(), wrap so a failed
- // permission response (main process busy / session dropped)
- // surfaces instead of dying as UnhandledPromiseRejection.
- if (activeIdRef.current !== sessionId) return;
- if (isSessionWorkspaceUnavailableError(error)) {
- showSessionWorkspaceUnavailableToast(toastApi, uiLocale, { sessionId });
- } else {
- toastApi.error(
- copy.responseFailedTitle,
- localizedShellErrorMessage(error, copy.responseFailedFallback, uiLocale),
- undefined,
- { sessionId },
- );
- }
- }
- }
-
- async function respondToUserQuestion(response: UserQuestionResponse) {
- const sessionId = activeIdRef.current;
- if (!sessionId) return;
- try {
- await window.maka.sessions.respondToUserQuestion(sessionId, response);
- onInteractionChanged?.(sessionId);
- setInteractionBySession((current) => dequeueInteractionByRequestId(current, sessionId, response.requestId));
- } catch (error) {
- if (activeIdRef.current !== sessionId) return;
- if (isSessionWorkspaceUnavailableError(error)) {
- showSessionWorkspaceUnavailableToast(toastApi, uiLocale, { sessionId });
- } else {
- toastApi.error(
- copy.responseFailedTitle,
- localizedShellErrorMessage(error, copy.responseFailedFallback, uiLocale),
- undefined,
- { sessionId },
- );
- }
- }
- }
-
- async function respondToUserForm(response: InteractionFormResponse) {
- const sessionId = activeIdRef.current;
- if (!sessionId) return;
- try {
- await window.maka.sessions.respondToUserForm(sessionId, response);
- onInteractionChanged?.(sessionId);
- setInteractionBySession((current) => dequeueInteractionByRequestId(current, sessionId, response.requestId));
} catch (error) {
if (activeIdRef.current !== sessionId) return;
if (isSessionWorkspaceUnavailableError(error)) {
@@ -860,9 +818,15 @@ export function createAppShellChatActions(deps: {
return {
send,
enqueueMessage,
- respondToSandboxBoundary,
- respondToUserQuestion,
- respondToUserForm,
+ respondToSandboxBoundary: (response) =>
+ respondToInteraction(
+ response,
+ window.maka.sessions.respondToSandboxBoundary,
+ onExecutionBoundaryChanged,
+ ),
+ respondToUserQuestion: (response) =>
+ respondToInteraction(response, window.maka.sessions.respondToUserQuestion),
+ respondToUserForm: (response) => respondToInteraction(response, submitUserForm),
refreshMessages,
retryMessages,
};
diff --git a/apps/desktop/src/renderer/app-shell-session-events.ts b/apps/desktop/src/renderer/app-shell-session-events.ts
index 62c3d478c6..ba9721d77a 100644
--- a/apps/desktop/src/renderer/app-shell-session-events.ts
+++ b/apps/desktop/src/renderer/app-shell-session-events.ts
@@ -365,9 +365,7 @@ export function createAppShellSessionEventHandlers(options: {
});
break;
case 'message_admission':
- if (event.outcome === 'retracted') {
- removeTransientMessage?.(sessionId, event.messageId);
- }
+ if (event.outcome === 'retracted') removeTransientMessage?.(sessionId, event.messageId);
break;
case 'steering_message':
// The live Turn projection now renders this same messageId in place.
@@ -448,9 +446,8 @@ export function createAppShellSessionEventHandlers(options: {
case 'complete': {
onInteractionChanged?.(sessionId);
setInteractionBySession((current) => clearInteractions(current, sessionId));
- if (event.contextCompactionOutcome) {
+ if (event.contextCompactionOutcome)
onContextCompactionOutcome?.(sessionId, event.turnId, event.contextCompactionOutcome);
- }
if (event.stopReason === 'end_turn' || event.stopReason === 'max_tokens') {
const body = [...(before?.steps ?? [])].reverse().find((step) => step.text?.text)?.text?.text;
notifyRunEnded?.({ kind: 'completed', sessionId, body });
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index a6e1c3af6f..3a28016f5f 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -826,7 +826,6 @@ function AppShellContent({
[sessions, onboarding.snapshot?.sessionSendOutcomes],
);
const activeInteraction = activeInteractionFor(interactionBySession, ownerActiveId);
- const activeForm = activeInteraction?.type === 'form_request' ? activeInteraction : undefined;
const activeSession = activeCatalogSession;
const activeMessageQueue = activeId ? messageQueueBySession[activeId] : undefined;
const activeMessageSubmitting = transientMessages.length > 0;
@@ -1848,6 +1847,7 @@ function AppShellContent({
setInteractionBySession: sessionUiController.setInteractionBySession,
onInteractionChanged: markInteractionChanged,
onExecutionBoundaryChanged: reloadActiveExecutionBoundary,
+ respondToUserForm: workbar.commands.respondToUserForm,
showModelSetupToast,
toastApi,
newChatModel: newChatModel ?? null,
@@ -2970,7 +2970,6 @@ function AppShellContent({
respondToSandboxBoundary={respondToSandboxBoundary}
respondToClientCapability={workbar.commands.respondToClientCapability}
respondToUserQuestion={respondToUserQuestion}
- activeForm={activeForm}
respondToUserForm={respondToUserForm}
stop={stop}
directoryComposerProps={directoryComposerProps}
diff --git a/apps/desktop/src/renderer/chat-composer-region.tsx b/apps/desktop/src/renderer/chat-composer-region.tsx
index 935c923598..ab90a2a7d9 100644
--- a/apps/desktop/src/renderer/chat-composer-region.tsx
+++ b/apps/desktop/src/renderer/chat-composer-region.tsx
@@ -113,7 +113,6 @@ interface ChatComposerRegionProps
respondToSandboxBoundary: ComponentProps<typeof SandboxBoundaryPrompt>['onRespond'];
respondToClientCapability: ComponentProps<typeof ClientCapabilityPrompt>['onRespond'];
respondToUserQuestion: ComponentProps<typeof UserQuestionPrompt>['onRespond'];
- activeForm: ComponentProps<typeof FormInteractionPrompt>['request'] | undefined;
respondToUserForm: ComponentProps<typeof FormInteractionPrompt>['onRespond'];
stop: ComponentProps<typeof UserQuestionPrompt>['onStop'];
boundaryUnreadableNotice?: BoundaryUnreadableNotice;
@@ -136,7 +135,6 @@ export function ChatComposerRegion({
respondToSandboxBoundary,
respondToClientCapability,
respondToUserQuestion,
- activeForm,
respondToUserForm,
stop,
boundaryUnreadableNotice,
@@ -149,7 +147,9 @@ export function ChatComposerRegion({
activeInteraction?.type === 'sandbox_boundary_request' ? activeInteraction : undefined;
const activeClientCapability =
activeInteraction?.type === 'client_capability_request' ? activeInteraction : undefined;
- const activeQuestion = activeInteraction?.type === 'user_question_request' ? activeInteraction : undefined;
+ const activeQuestion =
+ activeInteraction?.type === 'user_question_request' ? activeInteraction : undefined;
+ const activeForm = activeInteraction?.type === 'form_request' ? activeInteraction : undefined;
const previousNewTaskDraftKey = useRef(newTaskDraftKey);
useLayoutEffect(() => {
const previous = previousNewTaskDraftKey.current;
diff --git a/apps/desktop/src/renderer/features/workbar/controller/use-workbar-controller.ts b/apps/desktop/src/renderer/features/workbar/controller/use-workbar-controller.ts
index 688248f0a2..e31146d0d8 100644
--- a/apps/desktop/src/renderer/features/workbar/controller/use-workbar-controller.ts
+++ b/apps/desktop/src/renderer/features/workbar/controller/use-workbar-controller.ts
@@ -28,6 +28,7 @@ import {
} from 'react';
import type { ClientCapabilityResponse } from '@maka/core/client-capability-grant';
import type { QuoteRef } from '@maka/core/events';
+import type { InteractionFormResponse } from '@maka/core/interaction';
import type { SessionSummary } from '@maka/core/session';
import { Composer, useUiLocale } from '@maka/ui';
import type { ChatModelChoice } from '@maka/ui';
@@ -76,6 +77,7 @@ export interface WorkbarControllerCommands {
): void;
openSideChatWithQuote(quote: QuoteRef): void;
respondToClientCapability(response: ClientCapabilityResponse): Promise<void>;
+ respondToUserForm(sessionId: string, response: InteractionFormResponse): Promise<void>;
toggleRight(): void;
}
@@ -663,8 +665,20 @@ export function useWorkbarController(
);
const commands = useMemo<WorkbarControllerCommands>(
- () => ({ openTool, openSideChatWithQuote, respondToClientCapability, toggleRight }),
- [openSideChatWithQuote, openTool, respondToClientCapability, toggleRight],
+ () => ({
+ openTool,
+ openSideChatWithQuote,
+ respondToClientCapability,
+ respondToUserForm: sideChat.respondToUserForm,
+ toggleRight,
+ }),
+ [
+ openSideChatWithQuote,
+ openTool,
+ respondToClientCapability,
+ sideChat.respondToUserForm,
+ toggleRight,
+ ],
);
const activeSideChatTabIds = useMemo(
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org