fix(web): unlock v0.2 code agent composer steer (#805)

Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
Lyon
2026-06-04 11:02:40 +08:00
committed by GitHub
parent f2d4181496
commit 088414747b
9 changed files with 298 additions and 32 deletions
+38
View File
@@ -1262,6 +1262,44 @@ test("hwlab-cli agent composer status detects Web unlocked steer mode", async ()
assert.equal(result.payload.route.path, "/v1/agent/chat/steer");
});
test("hwlab-cli agent composer status ignores stale workspace activeTrace after selected conversation completed", async () => {
const result = await runHwlabCli([
"client",
"agent",
"composer",
"status",
"--base-url",
"http://web.test",
"--cookie",
"hwlab_session=session-a"
], {
fetchImpl: async () => new Response(JSON.stringify({
ok: true,
workspace: {
workspaceId: "wsp_composer",
revision: 10,
selectedConversationId: "cnv_completed",
selectedAgentSessionId: "ses_completed",
activeTraceId: "trc_stale_running",
workspace: {
activeTraceId: "trc_stale_running",
threadId: "thread-composer",
sessionStatus: "running",
messages: [{ role: "agent", status: "completed", traceId: "trc_completed", conversationId: "cnv_completed" }]
}
}
}), { status: 200 })
});
assert.equal(result.exitCode, 0);
assert.equal(result.payload.composer.locked, false);
assert.equal(result.payload.composer.disabled, false);
assert.equal(result.payload.composer.submitMode, "turn");
assert.equal(result.payload.composer.route, "/v1/agent/chat");
assert.equal(result.payload.composer.targetTraceId, null);
assert.equal(result.payload.composer.reason, "terminal-turn");
});
test("hwlab-cli agent composer submit auto routes active Web turn to steer", async () => {
const calls: any[] = [];
const result = await runHwlabCli([
+4 -2
View File
@@ -8,6 +8,8 @@ export function computeCodeAgentComposerState(input = {}) {
const current = input.currentRequest && typeof input.currentRequest === "object" ? input.currentRequest : null;
const workspace = input.workspace && typeof input.workspace === "object" ? input.workspace : null;
const explicitStatus = normalizedStatus(input.status ?? input.sessionStatus ?? workspace?.sessionStatus);
const latestStatus = normalizedStatus(latest?.status);
const effectiveStatus = latestStatus || explicitStatus;
const traceId = firstNonEmptyString(
input.targetTraceId,
current?.traceId,
@@ -15,7 +17,7 @@ export function computeCodeAgentComposerState(input = {}) {
workspace?.activeTraceId,
latest?.status === "running" ? latest?.traceId : null
);
const active = Boolean(traceId) && (
const active = Boolean(traceId) && !terminalStatus(effectiveStatus) && (
activeStatus(current?.status) ||
activeStatus(latest?.status) ||
activeStatus(explicitStatus) ||
@@ -51,7 +53,7 @@ export function computeCodeAgentComposerState(input = {}) {
threadId: firstNonEmptyString(input.threadId, current?.threadId, latest?.threadId, workspace?.threadId),
workspaceId: firstNonEmptyString(input.workspaceId, workspace?.workspaceId),
workspaceRevision: integerOrNull(input.workspaceRevision ?? workspace?.revision),
reason: active ? "active-turn-steer" : disabledReason ?? (terminalStatus(explicitStatus) ? "terminal-turn" : "idle-turn")
reason: active ? "active-turn-steer" : disabledReason ?? (terminalStatus(effectiveStatus) ? "terminal-turn" : "idle-turn")
});
}
+1 -1
View File
@@ -139,7 +139,7 @@ export function App(): ReactElement {
{route === "help" ? <HelpView help={help} /> : null}
{route === "settings" ? <SettingsView onLogout={auth.logout} /> : null}
<DraftsList onPick={pickDraft} />
<CommandBar disabled={store.composer.disabled} providerProfile={store.state.providerProfile} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} gatewayShellTimeoutMs={store.state.gatewayShellTimeoutMs} onProviderProfile={store.setProviderProfile} onCodeAgentTimeout={store.setCodeAgentTimeoutMs} onGatewayShellTimeout={store.setGatewayShellTimeoutMs} onSubmit={submitWithDraftRecord} onClear={store.clearConversation} pickedDraft={pickedDraft} onPickedDraftConsumed={clearPickedDraft} disabledReason={store.composer.disabledReason} onTyping={noteActivity} />
<CommandBar key={store.activeConversationId ?? "default"} disabled={store.composer.disabled} submitMode={store.composer.submitMode} targetTraceId={store.composer.targetTraceId} providerProfile={store.state.providerProfile} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} gatewayShellTimeoutMs={store.state.gatewayShellTimeoutMs} onProviderProfile={store.setProviderProfile} onCodeAgentTimeout={store.setCodeAgentTimeoutMs} onGatewayShellTimeout={store.setGatewayShellTimeoutMs} onSubmit={submitWithDraftRecord} onClear={store.clearConversation} pickedDraft={pickedDraft} onPickedDraftConsumed={clearPickedDraft} disabledReason={store.composer.disabledReason} onTyping={noteActivity} />
</section>
<div
className="resize-handle right-sidebar-resize"
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "bun:test";
const sourcePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "CommandBar.tsx");
test("CommandBar releases the send lock on the next tick instead of waiting for the full agent turn", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.match(source, /const submitted = props\.onSubmit\(next\);/u);
assert.match(source, /window\.setTimeout\(\(\) => setSubmitting\(false\), 0\);/u);
assert.doesNotMatch(source, /await props\.onSubmit\(next\)/u);
assert.doesNotMatch(source, /\.finally\(\(\) => setSubmitting\(false\)\)/u);
});
@@ -5,6 +5,8 @@ import type { ProviderProfile } from "../../types/domain";
interface CommandBarProps {
disabled: boolean;
submitMode: "turn" | "steer";
targetTraceId: string | null;
providerProfile: ProviderProfile;
codeAgentTimeoutMs: number;
gatewayShellTimeoutMs: number;
@@ -29,7 +31,7 @@ interface CommandBarProps {
export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason, onTyping, ...props }: CommandBarProps): ReactElement {
const [value, setValue] = useState("");
const [pending, setPending] = useState(false);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (pickedDraft) {
@@ -41,14 +43,12 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
async function submit(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
const next = value.trim();
if (!next || props.disabled || pending) return;
setPending(true);
try {
await props.onSubmit(next);
setValue("");
} finally {
setPending(false);
}
if (!next || props.disabled || submitting) return;
setValue("");
setSubmitting(true);
const submitted = props.onSubmit(next);
window.setTimeout(() => setSubmitting(false), 0);
void submitted.catch(() => setSubmitting(false));
}
function keyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
@@ -58,7 +58,7 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
}
return (
<form className="command-bar" id="command-form" aria-label="Agent 输入" onSubmit={submit} title={disabledReason ?? undefined}>
<form className="command-bar" id="command-form" aria-label="Agent 输入" onSubmit={submit} title={disabledReason ?? steerTitle(props.submitMode, props.targetTraceId)}>
<label className="agent-timeout-control" htmlFor="code-agent-provider-profile">
<span></span>
<select id="code-agent-provider-profile" aria-label="Code Agent 模型通道" value={props.providerProfile} onChange={(event) => props.onProviderProfile(event.currentTarget.value as ProviderProfile)}>
@@ -93,8 +93,18 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
<span className="prompt-mark">hwlab</span>
<textarea id="command-input" value={value} placeholder="输入任务,Enter 发送,Shift+Enter 换行" rows={1} onChange={(event) => { setValue(event.currentTarget.value); onTyping(); }} onKeyDown={keyDown} />
</div>
<button className="command-button" id="command-send" type="submit" disabled={props.disabled || pending}>{pending ? "发送中" : "发送"}</button>
<button className="command-button" id="command-send" type="submit" disabled={props.disabled || submitting}>{sendLabel(props.submitMode, submitting)}</button>
<button className="command-button secondary" id="command-clear" type="button" onClick={() => { setValue(""); props.onClear(); }}></button>
</form>
);
}
function sendLabel(submitMode: "turn" | "steer", submitting: boolean): string {
if (submitting) return submitMode === "steer" ? "引导中" : "发送中";
return submitMode === "steer" ? "引导" : "发送";
}
function steerTitle(submitMode: "turn" | "steer", targetTraceId: string | null): string | undefined {
if (submitMode !== "steer" || !targetTraceId) return undefined;
return `引导运行中的 turn: ${targetTraceId}`;
}
@@ -0,0 +1,152 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { composerFromState, workbenchReducer } from "./workbench-reducer";
import type { ChatMessage } from "../types/domain";
import type { WorkbenchState } from "./workbench-state";
test("composer stays unlocked and routes running turn input to steer", () => {
const state = baseState({
workspace: workspace("cnv_active", "trc_active"),
messages: [agentMessage({ conversationId: "cnv_active", traceId: "trc_active", status: "running" })],
chatPending: true
});
const composer = composerFromState(state, "cnv_active");
assert.equal(composer.disabled, false);
assert.equal(composer.submitMode, "steer");
assert.equal(composer.route, "/v1/agent/chat/steer");
assert.equal(composer.targetTraceId, "trc_active");
assert.equal(composer.conversationId, "cnv_active");
});
test("composer does not carry a running trace into another session", () => {
const state = baseState({
workspace: workspace("cnv_other", "trc_active"),
currentRequest: { traceId: "trc_active", conversationId: "cnv_active", sessionId: "ses_active", threadId: "thread_active", status: "running" },
messages: [agentMessage({ conversationId: "cnv_other", traceId: "trc_other_done", status: "completed" })],
chatPending: true
});
const composer = composerFromState(state, "cnv_other");
assert.equal(composer.disabled, false);
assert.equal(composer.submitMode, "turn");
assert.equal(composer.targetTraceId, null);
});
test("conversation selection scopes chatPending to the selected session messages", () => {
const state = baseState({
workspace: workspace("cnv_active", "trc_active"),
currentRequest: { traceId: "trc_active", conversationId: "cnv_active", sessionId: "ses_active", threadId: "thread_active", status: "running" },
messages: [agentMessage({ conversationId: "cnv_active", traceId: "trc_active", status: "running" })],
chatPending: true
});
const selected = workbenchReducer(state, {
type: "conversation:select",
conversation: {
conversationId: "cnv_done",
sessionId: "ses_done",
messages: [agentMessage({ conversationId: "cnv_done", traceId: "trc_done", status: "completed" })]
},
workspace: workspace("cnv_done", null)
});
assert.equal(selected.chatPending, false);
assert.equal(selected.currentRequest, null);
assert.equal(composerFromState(selected, "cnv_done").submitMode, "turn");
});
test("conversation selection restores steer mode when switching back to a running session", () => {
const state = baseState({ workspace: workspace("cnv_done", null), messages: [], chatPending: false });
const selected = workbenchReducer(state, {
type: "conversation:select",
conversation: {
conversationId: "cnv_active",
sessionId: "ses_active",
threadId: "thread_active",
messages: [agentMessage({ conversationId: "cnv_active", traceId: "trc_active", status: "running" })]
},
workspace: workspace("cnv_active", "trc_active")
});
const composer = composerFromState(selected, "cnv_active");
assert.equal(selected.chatPending, true);
assert.equal(composer.submitMode, "steer");
assert.equal(composer.targetTraceId, "trc_active");
});
test("composer routes to steer from workspace active status even before running message is restored", () => {
const state = baseState({
workspace: workspace("cnv_active", "trc_active"),
messages: [],
chatPending: false
});
const composer = composerFromState(state, "cnv_active");
assert.equal(composer.disabled, false);
assert.equal(composer.submitMode, "steer");
assert.equal(composer.targetTraceId, "trc_active");
});
test("composer disables only when no session or workspace exists", () => {
const composer = composerFromState(baseState({ workspace: null, messages: [] }), null);
assert.equal(composer.disabled, true);
assert.equal(composer.disabledReason, "session_required");
assert.equal(composer.submitMode, "turn");
});
function baseState(patch: Partial<WorkbenchState>): WorkbenchState {
return {
workspace: workspace("cnv_active", null),
conversations: [],
messages: [],
selectedDevicePodId: "device-pod-test",
providerProfile: "deepseek",
codeAgentTimeoutMs: 180_000,
gatewayShellTimeoutMs: 120_000,
live: null,
codeAgentAvailability: null,
loading: false,
chatPending: false,
currentRequest: null,
error: null,
...patch
};
}
function workspace(selectedConversationId: string, activeTraceId: string | null): WorkbenchState["workspace"] {
return {
workspaceId: "wsp_test",
revision: 1,
selectedConversationId,
selectedAgentSessionId: "ses_test",
activeTraceId,
workspace: {
selectedConversationId,
selectedAgentSessionId: "ses_test",
threadId: "thread_test",
activeTraceId,
sessionStatus: activeTraceId ? "running" : "completed"
}
};
}
function agentMessage(input: { conversationId: string; traceId: string; status: ChatMessage["status"] }): ChatMessage {
return {
id: `msg_${input.traceId}`,
role: "agent",
title: "Code Agent",
text: "message",
status: input.status,
createdAt: "2026-06-04T00:00:00.000Z",
traceId: input.traceId,
conversationId: input.conversationId,
sessionId: input.conversationId.replace("cnv", "ses"),
threadId: input.conversationId.replace("cnv", "thread")
};
}
@@ -12,7 +12,7 @@ export type Action =
| { type: "profile:set"; profile: WorkbenchState["providerProfile"] }
| { type: "timeout:set"; codeAgentTimeoutMs: number }
| { type: "gateway-timeout:set"; gatewayShellTimeoutMs: number }
| { type: "message:pending"; user: ChatMessage; pending: ChatMessage; request: WorkbenchState["currentRequest"] }
| { type: "message:pending"; user: ChatMessage; pending: ChatMessage; request: NonNullable<WorkbenchState["currentRequest"]> }
| { type: "message:replace"; messageId: string; message: ChatMessage }
| { type: "message:trace-status"; messageId: string; status: string }
| { type: "message:trace"; messageId: string; trace: NonNullable<ChatMessage["runnerTrace"]> }
@@ -33,7 +33,7 @@ export function workbenchReducer(state: WorkbenchState, action: Action): Workben
case "profile:set": return { ...state, providerProfile: action.profile };
case "timeout:set": return { ...state, codeAgentTimeoutMs: action.codeAgentTimeoutMs };
case "gateway-timeout:set": return { ...state, gatewayShellTimeoutMs: action.gatewayShellTimeoutMs };
case "message:pending": return { ...state, messages: [...state.messages, action.user, action.pending], chatPending: true, currentRequest: action.request };
case "message:pending": return { ...state, messages: [...state.messages, action.user, action.pending], chatPending: true, currentRequest: { ...action.request, status: "running" } };
case "message:replace": return { ...state, messages: replaceMessage(state.messages, action.messageId, action.message) };
case "message:trace-status": {
const updated = state.messages.map((message) => message.id === action.messageId ? { ...message, traceReplayStatus: action.status, updatedAt: new Date().toISOString() } : message);
@@ -45,8 +45,11 @@ export function workbenchReducer(state: WorkbenchState, action: Action): Workben
}
case "message:complete": return { ...state, messages: replaceMessage(state.messages, action.messageId, action.message), codeAgentAvailability: action.availability ?? state.codeAgentAvailability, workspace: action.workspace ?? state.workspace };
case "message:fail": return { ...state, messages: replaceMessage(state.messages, action.messageId, action.message) };
case "chat:done": return { ...state, chatPending: false, currentRequest: null };
case "conversation:select": return { ...state, workspace: action.workspace ?? state.workspace, messages: action.conversation?.messages ?? [], error: null };
case "chat:done": return { ...state, chatPending: false, currentRequest: state.currentRequest ? { ...state.currentRequest, status: "completed" } : null };
case "conversation:select": {
const messages = action.conversation?.messages ?? [];
return { ...state, workspace: action.workspace ?? state.workspace, messages, chatPending: hasRunningAgentMessage(messages), currentRequest: null, error: null };
}
case "conversation:list": return { ...state, conversations: action.conversations };
case "conversation:clear": return { ...state, messages: [], currentRequest: null, chatPending: false };
default: return state;
@@ -58,16 +61,56 @@ function replaceMessage(messages: ChatMessage[], messageId: string, replacement:
}
export function composerFromState(state: WorkbenchState, activeConversationId: string | null): ComposerState {
const activeTraceId = firstNonEmptyString(state.currentRequest?.traceId, state.workspace?.activeTraceId, state.workspace?.workspace?.activeTraceId);
const sessionStatus = firstNonEmptyString(state.messages.at(-1)?.status, state.workspace?.workspace?.sessionStatus);
const canSteer = Boolean(activeTraceId && state.chatPending && sessionStatus !== "completed" && sessionStatus !== "failed");
const latestMessage = latestConversationMessage(state.messages, activeConversationId);
const runningMessage = findRunningAgentMessage(state.messages, activeConversationId);
const currentRequestInConversation = state.currentRequest && state.currentRequest.conversationId === activeConversationId ? state.currentRequest : null;
const activeTraceId = firstNonEmptyString(currentRequestInConversation?.traceId, runningMessage?.traceId, runningMessage?.runnerTrace?.traceId, state.workspace?.activeTraceId, state.workspace?.workspace?.activeTraceId);
const workspaceStatus = firstNonEmptyString(state.workspace?.workspace?.sessionStatus);
const sessionStatus = firstNonEmptyString(runningMessage?.status, currentRequestInConversation?.status, latestMessage?.status, workspaceStatus);
const latestTerminal = isTerminalStatus(latestMessage?.status);
const terminal = latestTerminal || isTerminalStatus(currentRequestInConversation?.status) || (!latestMessage && isTerminalStatus(workspaceStatus));
const activeByStatus = isActiveStatus(runningMessage?.status) || isActiveStatus(runningMessage?.runnerTrace?.status) || isActiveStatus(currentRequestInConversation?.status) || isActiveStatus(workspaceStatus) || state.chatPending;
const canSteer = Boolean(activeConversationId && activeTraceId && !terminal && activeByStatus);
const conversationId = activeConversationId;
const sessionId = firstNonEmptyString(runningMessage?.sessionId, currentRequestInConversation?.sessionId, state.workspace?.selectedAgentSessionId, state.workspace?.workspace?.selectedAgentSessionId);
const threadId = firstNonEmptyString(runningMessage?.threadId, currentRequestInConversation?.threadId, state.workspace?.workspace?.threadId);
if (canSteer) {
return { disabled: false, disabledReason: null, submitMode: "steer", route: "/v1/agent/chat/steer", targetTraceId: activeTraceId };
return { disabled: false, disabledReason: null, submitMode: "steer", route: "/v1/agent/chat/steer", targetTraceId: activeTraceId, conversationId, sessionId, threadId };
}
if (!activeConversationId && !state.workspace?.workspaceId) {
return { disabled: true, disabledReason: "session_required", submitMode: "turn", route: "/v1/agent/chat", targetTraceId: null };
return { disabled: true, disabledReason: "session_required", submitMode: "turn", route: "/v1/agent/chat", targetTraceId: null, conversationId, sessionId, threadId };
}
return { disabled: false, disabledReason: null, submitMode: "turn", route: "/v1/agent/chat", targetTraceId: null };
return { disabled: false, disabledReason: null, submitMode: "turn", route: "/v1/agent/chat", targetTraceId: null, conversationId, sessionId, threadId };
}
function findRunningAgentMessage(messages: ChatMessage[], activeConversationId: string | null): ChatMessage | null {
for (const message of [...messages].reverse()) {
if (message.role !== "agent") continue;
if (message.status !== "running" && message.runnerTrace?.status !== "running") continue;
if (activeConversationId && message.conversationId && message.conversationId !== activeConversationId) continue;
return message;
}
return null;
}
function latestConversationMessage(messages: ChatMessage[], activeConversationId: string | null): ChatMessage | null {
for (const message of [...messages].reverse()) {
if (activeConversationId && message.conversationId && message.conversationId !== activeConversationId) continue;
return message;
}
return null;
}
function hasRunningAgentMessage(messages: ChatMessage[]): boolean {
return Boolean(findRunningAgentMessage(messages, null));
}
function isActiveStatus(value: unknown): boolean {
return ["running", "pending", "accepted", "processing"].includes(String(value ?? "").trim().toLowerCase());
}
function isTerminalStatus(value: unknown): boolean {
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled"].includes(String(value ?? "").trim().toLowerCase());
}
export function availabilityFromLive(live: LiveSurface): CodeAgentAvailability | null {
@@ -14,7 +14,7 @@ export interface WorkbenchState {
codeAgentAvailability: CodeAgentAvailability | null;
loading: boolean;
chatPending: boolean;
currentRequest: { traceId: string; conversationId: string; sessionId: string | null; threadId: string | null } | null;
currentRequest: { traceId: string; conversationId: string; sessionId: string | null; threadId: string | null; status?: "running" | "completed" } | null;
error: string | null;
}
@@ -24,6 +24,9 @@ export interface ComposerState {
submitMode: "turn" | "steer";
route: "/v1/agent/chat" | "/v1/agent/chat/steer";
targetTraceId: string | null;
conversationId: string | null;
sessionId: string | null;
threadId: string | null;
}
export interface WorkbenchRuntimeContext {
+10 -7
View File
@@ -229,15 +229,17 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
const submitMessage = useCallback(async (text: string) => {
const value = text.trim();
if (!value) return;
const traceId = nextProtocolId("trc");
const conversationId = activeConversationId ?? nextProtocolId("cnv");
const sessionId = state.workspace?.selectedAgentSessionId ?? state.workspace?.workspace?.selectedAgentSessionId ?? null;
const threadId = state.workspace?.workspace?.threadId ?? null;
const steerMode = composer.submitMode === "steer";
const traceId = steerMode && composer.targetTraceId ? composer.targetTraceId : nextProtocolId("trc");
const steerTraceId = steerMode ? nextProtocolId("trc_steer") : null;
const conversationId = composer.conversationId ?? activeConversationId ?? nextProtocolId("cnv");
const sessionId = composer.sessionId ?? state.workspace?.selectedAgentSessionId ?? state.workspace?.workspace?.selectedAgentSessionId ?? null;
const threadId = composer.threadId ?? state.workspace?.workspace?.threadId ?? null;
const user = makeMessage("user", value, "sent", { traceId, conversationId, sessionId, threadId });
const pending = makeMessage("agent", "正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent 处理中" });
const pendingWithRetry: ChatMessage = { ...pending, retryInput: value };
dispatch({ type: "message:pending", user, pending: pendingWithRetry, request: { traceId, conversationId, sessionId, threadId } });
const route = composer.submitMode === "steer" ? api.steerAgentMessage : api.sendAgentMessage;
const route = steerMode ? api.steerAgentMessage : api.sendAgentMessage;
// The submit call itself is a "kicked off" event; treat it as activity
// so the initial POST has a fresh inactivity window.
updateActivity();
@@ -254,7 +256,8 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
gatewayShellTimeoutMs: state.gatewayShellTimeoutMs,
workspaceId: state.workspace?.workspaceId,
workspaceRevision: state.workspace?.revision,
targetTraceId: composer.targetTraceId
targetTraceId: steerMode ? traceId : null,
steerTraceId
}, state.codeAgentTimeoutMs, submitActivityRef);
const finalize = async (result: AgentChatResultResponse, availability: CodeAgentAvailability | null): Promise<void> => {
const completed = messageFromAgentResponse(pending.id, pending, result);
@@ -287,7 +290,7 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
dispatch({ type: "message:fail", messageId: pending.id, message: failed });
}
dispatch({ type: "chat:done" });
}, [activeConversationId, composer.submitMode, composer.targetTraceId, state.codeAgentTimeoutMs, state.gatewayShellTimeoutMs, state.messages, state.providerProfile, state.workspace]);
}, [activeConversationId, composer.conversationId, composer.sessionId, composer.submitMode, composer.targetTraceId, composer.threadId, state.codeAgentTimeoutMs, state.gatewayShellTimeoutMs, state.messages, state.providerProfile, state.workspace]);
const cancelAgentMessage = useCallback(async (messageId: string) => {
return cancelAgentMessageAction(messageId, {