fix(web): share workbench terminal authority helpers
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
import type { ChatMessage, ProjectionDiagnostic, ProviderProfile, WorkbenchSessionRecord } from "@/types";
|
||||
import { firstNonEmptyString } from "@/utils";
|
||||
import { messageHasSealedTerminalResult, messageIsSealedTerminal, turnStatusAuthorityIsSealed } from "./workbench-terminal-authority";
|
||||
|
||||
export interface ComposerState {
|
||||
disabled: boolean;
|
||||
@@ -82,10 +83,10 @@ export function resolveComposerState(input: { messages: ChatMessage[]; sessions?
|
||||
const turn = activeTraceId ? input.turnStatusAuthority?.[activeTraceId] : null;
|
||||
const activeByRequest = Boolean(input.chatPending && currentRequest && isActiveStatus(currentRequest.status ?? "running"));
|
||||
const activeByMessage = latestMessage?.role === "agent" && isActiveStatus(latestMessage.status);
|
||||
const terminalByMessage = messageIsTerminalForSession(latestMessage);
|
||||
const terminalByMessage = messageIsSealedTerminal(latestMessage);
|
||||
const turnStatus = normalizeSessionStatus(turn?.status);
|
||||
const terminalTurn = turn?.terminal === true || isTerminalStatus(turnStatus);
|
||||
const terminalByTurn = terminalTurn && messageHasTerminalResponse(latestMessage);
|
||||
const terminalByTurn = terminalTurn && messageHasSealedTerminalResult(latestMessage);
|
||||
const activeByStatus = !terminalByMessage && !terminalByTurn && (turn?.running === true || isActiveStatus(turnStatus) || activeByRequest || activeByMessage);
|
||||
const terminal = terminalByMessage || terminalByTurn;
|
||||
const active = activeSession(input.sessions ?? [], sessionId);
|
||||
@@ -106,7 +107,7 @@ export function resolveCancelableAgentMessage(input: { messages: ChatMessage[];
|
||||
if (message.role !== "agent") continue;
|
||||
if (firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) !== targetTraceId) continue;
|
||||
if (!messageBelongsToCancelTarget(message, input)) continue;
|
||||
if (messageIsTerminalForSession(message)) continue;
|
||||
if (messageIsSealedTerminal(message)) continue;
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
@@ -312,27 +313,10 @@ function traceNeedsTurnStatusRefresh(traceId: string | null | undefined, message
|
||||
const turn = turnStatusAuthority?.[id];
|
||||
const message = latestMessageForTrace(messages, id);
|
||||
if (turnStatusAuthorityIsSealed(turn, message)) return false;
|
||||
if (messageIsTerminalForSession(message)) return false;
|
||||
if (messageIsSealedTerminal(message)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function turnStatusAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null): boolean {
|
||||
if (!(turn?.terminal === true || isTerminalStatus(turn?.status))) return false;
|
||||
return message ? messageHasTerminalResponse(message) : false;
|
||||
}
|
||||
|
||||
function messageIsTerminalForSession(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
const status = normalizeSessionStatus(message.status);
|
||||
return isTerminalStatus(status) && messageHasTerminalResponse(message);
|
||||
}
|
||||
|
||||
function messageHasTerminalResponse(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
const finalResponse = recordValue((message as Record<string, unknown>).finalResponse);
|
||||
return Boolean(firstNonEmptyString(message.text, message.content, finalResponse?.text, finalResponse?.content, finalResponse?.message));
|
||||
}
|
||||
|
||||
function resolveSessionTabStatus(session: WorkbenchSessionRecord, authority: SessionStatusAuthority | null | undefined): string {
|
||||
void session;
|
||||
const authorityStatus = normalizeSessionStatus(authority?.status);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed, turnStatusAuthorityIsSealed } from "./workbench-terminal-authority";
|
||||
|
||||
test("terminal authority requires terminal status and a final body for sealed messages", () => {
|
||||
assert.equal(messageIsSealedTerminal({ role: "agent", status: "completed", text: "final answer" } as any), true);
|
||||
assert.equal(messageIsSealedTerminal({ role: "agent", status: "completed" } as any), false);
|
||||
assert.equal(messageIsSealedTerminal({ role: "agent", status: "running", text: "partial" } as any), false);
|
||||
});
|
||||
|
||||
test("turn authority seals when terminal turn has final response even if message status is stale running", () => {
|
||||
const turn = { traceId: "trc_terminal", status: "completed", terminal: true, running: false } as any;
|
||||
const message = { role: "agent", status: "running", traceId: "trc_terminal", finalResponse: { text: "final answer" } } as any;
|
||||
|
||||
assert.equal(turnStatusAuthorityIsSealed(turn, message), true);
|
||||
assert.equal(traceAuthorityIsSealed(turn, message), true);
|
||||
});
|
||||
|
||||
test("terminal result text can come from reply and finalText authority fields", () => {
|
||||
assert.equal(messageHasSealedTerminalResult({ role: "agent", status: "running", reply: { finalText: "done" } } as any), true);
|
||||
assert.equal(messageHasSealedTerminalResult({ role: "agent", status: "running", finalText: "done" } as any), true);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection.
|
||||
// Responsibility: Shared terminal seal authority helpers for Workbench store reducers.
|
||||
|
||||
import type { ChatMessage } from "@/types";
|
||||
import { firstNonEmptyString } from "@/utils";
|
||||
import { finalResponseText, messageText, normalizedStatusText } from "./workbench-message-projection-runtime";
|
||||
import type { TurnStatusAuthority } from "./workbench-session";
|
||||
|
||||
export function traceAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null | undefined): boolean {
|
||||
return turnStatusAuthorityIsSealed(turn, message);
|
||||
}
|
||||
|
||||
export function turnStatusAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null | undefined): boolean {
|
||||
if (!(turn?.terminal === true || isTerminalAuthorityStatus(turn?.status))) return false;
|
||||
return messageHasSealedTerminalResult(message);
|
||||
}
|
||||
|
||||
export function messageIsSealedTerminal(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
return isTerminalAuthorityStatus(message.status) && messageHasSealedTerminalResult(message);
|
||||
}
|
||||
|
||||
export function messageHasSealedTerminalResult(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
const record = message as Record<string, unknown>;
|
||||
const reply = recordValue(record.reply);
|
||||
return Boolean(firstNonEmptyString(
|
||||
messageText(message.text),
|
||||
messageText(record.content),
|
||||
finalResponseText(record.finalResponse),
|
||||
finalResponseText(record.reply),
|
||||
messageText(reply?.finalText),
|
||||
messageText(record.finalText),
|
||||
messageText(record.final)
|
||||
));
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function isTerminalAuthorityStatus(value: unknown): boolean {
|
||||
return [
|
||||
"completed",
|
||||
"failed",
|
||||
"blocked",
|
||||
"timeout",
|
||||
"canceled",
|
||||
"stale",
|
||||
"thread-resume-failed",
|
||||
"interrupted",
|
||||
"expired"
|
||||
].includes(normalizeTerminalAuthorityStatus(value) ?? "");
|
||||
}
|
||||
|
||||
function normalizeTerminalAuthorityStatus(value: unknown): string | null {
|
||||
const status = normalizedStatusText(value);
|
||||
if (status === "cancelled") return "canceled";
|
||||
if (status === "error") return "failed";
|
||||
return status;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
|
||||
import { cleanupWorkbenchServerStateSessions, createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
|
||||
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
|
||||
import { reduceWorkbenchRealtimeEvent, type WorkbenchRealtimeAction } from "./workbench-event-reducer";
|
||||
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed } from "./workbench-terminal-authority";
|
||||
import {
|
||||
agentErrorDisplayText,
|
||||
agentErrorFromApiFailure,
|
||||
@@ -1438,10 +1439,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
}));
|
||||
}
|
||||
|
||||
function messageHasSealedTerminalResult(message: ChatMessage | null): boolean {
|
||||
return messageHasTerminalResponse(message);
|
||||
}
|
||||
|
||||
function failTrace(traceId: string, message: string): void {
|
||||
markMessage(traceId, { status: "failed", text: message });
|
||||
chatPending.value = false;
|
||||
|
||||
Reference in New Issue
Block a user