mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Derive message stream display data from semantic items
This commit is contained in:
parent
4185cd3899
commit
45edfba92b
37 changed files with 859 additions and 606 deletions
|
|
@ -9,7 +9,7 @@ import type { ThreadManagementActions } from "../threads/thread-management-actio
|
|||
import type { GoalActions } from "../threads/goal-actions";
|
||||
import type { HistoryController } from "../threads/history-controller";
|
||||
import type { ChatInboundController } from "../protocol/inbound/controller";
|
||||
import type { MessageStreamDetailSection } from "../message-stream/items";
|
||||
import type { MessageStreamNoticeSection } from "../message-stream/items";
|
||||
import type { ChatMessageScrollIntentState } from "../ui/message-stream/scroll-intent-state";
|
||||
import type { ComposerMetaViewModel } from "../ui/composer";
|
||||
import type { ChatPanelComposerShellState } from "../ui/shell-state";
|
||||
|
|
@ -39,7 +39,7 @@ interface ConversationPartsContext {
|
|||
};
|
||||
};
|
||||
runtime: {
|
||||
connectionDiagnosticDetails: () => MessageStreamDetailSection[];
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
statusSummaryLines: () => string[];
|
||||
|
|
@ -55,7 +55,7 @@ interface ConversationPartsContext {
|
|||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamDetailSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
};
|
||||
scroll: {
|
||||
forceBottom: () => void;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { approvalActionKind, type ApprovalAction, type PendingApproval } from "../../protocol/server-requests/approval";
|
||||
import { approvalDetails, approvalResultSummary, approvalTitle } from "./approval-view";
|
||||
import type { MessageStreamDetailSection, MessageStreamItem } from "../../message-stream/items";
|
||||
import type { MessageStreamItem } from "../../message-stream/items";
|
||||
import type { PendingUserInput } from "../../protocol/server-requests/user-input";
|
||||
import { definedProp } from "../../../../utils";
|
||||
|
||||
|
|
@ -15,17 +15,12 @@ export function createApprovalResultItem(approval: PendingApproval, action: Appr
|
|||
text: approvalResultText(approval, action),
|
||||
...definedProp("turnId", approvalTurnId(approval)),
|
||||
executionState: kind === "accept" || kind === "accept-session" ? "completed" : "failed",
|
||||
details: [
|
||||
{
|
||||
title: "Approval",
|
||||
rows: [
|
||||
{ key: "status", value: status },
|
||||
{ key: "scope", value: scope },
|
||||
{ key: "request", value: approvalTitle(approval) },
|
||||
...approvalDetails(approval),
|
||||
],
|
||||
},
|
||||
],
|
||||
approval: {
|
||||
status,
|
||||
scope,
|
||||
request: approvalTitle(approval),
|
||||
auditFacts: approvalDetails(approval),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -36,12 +31,11 @@ export function createUserInputResultItem(
|
|||
): MessageStreamItem {
|
||||
const questionCount = input.params.questions.length;
|
||||
const label = questionCount === 1 ? "1 question" : `${String(questionCount)} questions`;
|
||||
const details: MessageStreamDetailSection[] = input.params.questions.map((question) => ({
|
||||
title: `Question: ${question.header || question.id}`,
|
||||
rows: [
|
||||
{ key: "Prompt", value: question.question },
|
||||
...(status === "submitted" ? [{ key: "Answer", value: answers[question.id] ?? "" }] : []),
|
||||
],
|
||||
const questions = input.params.questions.map((question) => ({
|
||||
id: question.id,
|
||||
header: question.header || question.id,
|
||||
question: question.question,
|
||||
...(status === "submitted" ? { answer: answers[question.id] ?? "" } : {}),
|
||||
}));
|
||||
return {
|
||||
id: `user-input-${status}-${String(input.requestId)}`,
|
||||
|
|
@ -50,7 +44,7 @@ export function createUserInputResultItem(
|
|||
text: status === "submitted" ? `Input submitted for ${label}.` : `Input request cancelled for ${label}.`,
|
||||
...definedProp("turnId", input.params.turnId),
|
||||
executionState: status === "submitted" ? "completed" : "failed",
|
||||
details,
|
||||
questions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { AppServerClient } from "../../../../app-server/connection/client";
|
|||
import type { CodexInput } from "../../../../domain/chat/input";
|
||||
import type { ChatServerThreadActions } from "../../connection/server-actions/threads";
|
||||
import type { ChatReconnectActions } from "../../connection/reconnect-actions";
|
||||
import type { MessageStreamDetailSection } from "../../message-stream/items";
|
||||
import type { MessageStreamNoticeSection } from "../../message-stream/items";
|
||||
import type { ChatRuntimeSettingsActions } from "../../runtime/settings-actions";
|
||||
import type { ChatStateStore } from "../../state/reducer";
|
||||
import type { ThreadManagementActions } from "../../threads/thread-management-actions";
|
||||
|
|
@ -22,10 +22,10 @@ export interface ConversationTurnActionsContext {
|
|||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamDetailSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
};
|
||||
runtime: {
|
||||
connectionDiagnosticDetails: () => MessageStreamDetailSection[];
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
statusSummaryLines: () => string[];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
type SlashCommandName,
|
||||
type SlashCommandSubcommandDefinition,
|
||||
} from "../composer/slash-commands";
|
||||
import type { MessageStreamDetailSection, MessageStreamDetailMetaRow } from "../../message-stream/items";
|
||||
import type { MessageStreamAuditFact, MessageStreamNoticeSection } from "../../message-stream/items";
|
||||
import { modelOverrideMessage, reasoningEffortOverrideMessage } from "../../runtime/messages";
|
||||
import { currentThreadReferenceMessage } from "./messages";
|
||||
import {
|
||||
|
|
@ -42,7 +42,7 @@ export interface SlashCommandExecutionContext {
|
|||
toggleCollaborationMode: () => void | Promise<void>;
|
||||
toggleAutoReview: () => void | Promise<void>;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamDetailSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
requestModel: (model: string) => boolean | undefined | Promise<boolean | undefined>;
|
||||
resetModelToConfig: () => boolean | undefined | Promise<boolean | undefined>;
|
||||
requestReasoningEffort: (effort: ReasoningEffort) => boolean | undefined | Promise<boolean | undefined>;
|
||||
|
|
@ -53,7 +53,7 @@ export interface SlashCommandExecutionContext {
|
|||
setGoalStatus: (threadId: string, status: ThreadGoalStatus) => Promise<boolean>;
|
||||
clearGoal: (threadId: string) => Promise<boolean>;
|
||||
statusSummaryLines: () => string[];
|
||||
connectionDiagnosticDetails: () => MessageStreamDetailSection[];
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
|
|
@ -371,8 +371,8 @@ function goalUsageError(message: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
function goalDetails(goal: ThreadGoal): MessageStreamDetailSection[] {
|
||||
const rows: MessageStreamDetailMetaRow[] = [
|
||||
function goalDetails(goal: ThreadGoal): MessageStreamNoticeSection[] {
|
||||
const auditFacts: MessageStreamAuditFact[] = [
|
||||
{ key: "status", value: goal.status },
|
||||
{ key: "objective", value: goal.objective },
|
||||
{
|
||||
|
|
@ -381,7 +381,7 @@ function goalDetails(goal: ThreadGoal): MessageStreamDetailSection[] {
|
|||
},
|
||||
{ key: "elapsed", value: formatGoalElapsed(goal.timeUsedSeconds) },
|
||||
];
|
||||
return [{ rows }];
|
||||
return [{ auditFacts }];
|
||||
}
|
||||
|
||||
function formatGoalElapsed(seconds: number): string {
|
||||
|
|
@ -399,13 +399,13 @@ function usageError(command: SlashCommandName, message: string): string {
|
|||
return `${definition.command} ${message}. Usage: ${definition.usage}`;
|
||||
}
|
||||
|
||||
function detailsFromLines(lines: string[]): MessageStreamDetailSection[] {
|
||||
function detailsFromLines(lines: string[]): MessageStreamNoticeSection[] {
|
||||
const first = lines[0] ?? "";
|
||||
const content = first.includes(": ") ? lines : lines.slice(1);
|
||||
return [{ rows: content.map(lineToRow) }];
|
||||
return [{ auditFacts: content.map(lineToRow) }];
|
||||
}
|
||||
|
||||
function lineToRow(line: string): MessageStreamDetailMetaRow {
|
||||
function lineToRow(line: string): MessageStreamAuditFact {
|
||||
const separator = line.indexOf(": ");
|
||||
if (separator > 0) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -41,13 +41,10 @@ interface MessageStreamCollabAgentState {
|
|||
|
||||
export function agentMessageStreamItem(item: MessageStreamCollabAgentToolCall, turnId?: string): AgentMessageStreamItem {
|
||||
const agents = agentStatesDisplay(item.agentsStates);
|
||||
const receiverText = item.receiverThreadIds.length > 0 ? `\ntargets: ${item.receiverThreadIds.join(", ")}` : "";
|
||||
const promptText = item.prompt ? `\n${item.prompt}` : "";
|
||||
return {
|
||||
id: item.id,
|
||||
kind: "agent",
|
||||
role: "tool",
|
||||
text: `${agentActivitySummaryLabel(item.tool)}\nstatus: ${item.status}${receiverText}${promptText}`,
|
||||
...definedProp("turnId", turnId),
|
||||
sourceItemId: item.id,
|
||||
tool: item.tool,
|
||||
|
|
@ -62,15 +59,6 @@ export function agentMessageStreamItem(item: MessageStreamCollabAgentToolCall, t
|
|||
};
|
||||
}
|
||||
|
||||
function agentActivitySummaryLabel(tool: string): string {
|
||||
if (tool === "spawnAgent") return "Spawn agent";
|
||||
if (tool === "sendInput") return "Send input to agent";
|
||||
if (tool === "resumeAgent") return "Resume agent";
|
||||
if (tool === "wait") return "Wait for agent";
|
||||
if (tool === "closeAgent") return "Close agent";
|
||||
return `Agent ${tool}`;
|
||||
}
|
||||
|
||||
function agentStatesDisplay(states: MessageStreamCollabAgentToolCall["agentsStates"]): AgentStateSummary[] {
|
||||
return Object.entries(states)
|
||||
.map(([threadId, state]) => ({
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import { jsonPreview, truncate } from "../../../utils";
|
||||
import type { MessageStreamDetailMetaRow, MessageStreamDetailSection } from "./items";
|
||||
|
||||
const TOOL_SUMMARY_LIMIT = 140;
|
||||
|
||||
export function compactToolSummary(label: string | null, target?: string | null, qualifier?: string | null): string {
|
||||
const targetText = target?.trim();
|
||||
const base = label ? (targetText ? `${label}: ${targetText}` : label) : (targetText ?? "details");
|
||||
return truncate(qualifier ? `${base} (${qualifier})` : base, TOOL_SUMMARY_LIMIT);
|
||||
}
|
||||
|
||||
export function statusQualifier(status: unknown, failure?: string | null): string | null {
|
||||
if (status === "declined") return "declined";
|
||||
if (status === "failed") return failure && failure.length > 0 ? failure : "failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function failedStatusLabel(status: unknown): string | null {
|
||||
if (status === "failed") return "failed";
|
||||
if (status === "declined") return "declined";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function metaDetail(title: string, rows: MessageStreamDetailMetaRow[]): MessageStreamDetailSection[] {
|
||||
return rows.length > 0 ? [{ title, rows }] : [];
|
||||
}
|
||||
|
||||
export function bodyDetail(title: string, body: string | null | undefined): MessageStreamDetailSection[] {
|
||||
return body ? [{ title, body }] : [];
|
||||
}
|
||||
|
||||
function jsonDetail(title: string, value: unknown): MessageStreamDetailSection[] {
|
||||
return value === null || value === undefined ? [] : [{ title, body: jsonPreview(value) }];
|
||||
}
|
||||
|
||||
export function jsonDetails(entries: [title: string, value: unknown][]): MessageStreamDetailSection[] {
|
||||
return entries.flatMap(([title, value]) => jsonDetail(title, value));
|
||||
}
|
||||
|
||||
export function jsonTargetLabel(value: unknown): string | null {
|
||||
const direct = jsonTargetPrimitive(value);
|
||||
if (direct) return direct;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const priorityKeys = [
|
||||
"q",
|
||||
"query",
|
||||
"search_query",
|
||||
"url",
|
||||
"ref_id",
|
||||
"path",
|
||||
"file",
|
||||
"filename",
|
||||
"ticker",
|
||||
"location",
|
||||
"team",
|
||||
"league",
|
||||
"id",
|
||||
"target",
|
||||
"command",
|
||||
];
|
||||
|
||||
for (const key of priorityKeys) {
|
||||
const target = jsonTargetPrimitive(record[key]);
|
||||
if (target) return target;
|
||||
}
|
||||
|
||||
const firstEntry = Object.entries(record).find(([, entryValue]) => jsonTargetPrimitive(entryValue));
|
||||
return firstEntry ? jsonTargetPrimitive(firstEntry[1]) : null;
|
||||
}
|
||||
|
||||
function jsonTargetPrimitive(value: unknown): string | null {
|
||||
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (!Array.isArray(value)) return null;
|
||||
for (const item of value) {
|
||||
const target = jsonTargetLabel(item);
|
||||
if (target) return target;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,29 +1,21 @@
|
|||
import type {
|
||||
MessageStreamDetailSection,
|
||||
MessageStreamFileChange,
|
||||
MessageStreamFileMention,
|
||||
MessageStreamItem,
|
||||
CommandMessageStreamTarget,
|
||||
ExecutionState,
|
||||
MessageStreamPrimaryTarget,
|
||||
} from "./items";
|
||||
import type { HistoricalTurn } from "../../../domain/threads/history";
|
||||
import type { FileUpdateChange, TurnItem } from "../../../app-server/protocol/turn";
|
||||
import { definedProp, truncate } from "../../../utils";
|
||||
import { definedProp } from "../../../utils";
|
||||
import { referencedThreadMetadataFromPrompt, type ReferencedThreadMetadata } from "../../../domain/threads/reference";
|
||||
import { turnUserItemText } from "../../../app-server/protocol/turn";
|
||||
import { agentMessageStreamItem } from "./agent-items";
|
||||
import { fileMentionsFromInput } from "./file-mentions";
|
||||
import { normalizeProposedPlanMarkdown } from "./proposed-plan";
|
||||
import { pathRelativeToRoot } from "./path-labels";
|
||||
import { userMessageDisplayText } from "./user-message-text";
|
||||
import {
|
||||
bodyDetail,
|
||||
compactToolSummary,
|
||||
failedStatusLabel,
|
||||
jsonDetails,
|
||||
jsonTargetLabel,
|
||||
metaDetail,
|
||||
statusQualifier,
|
||||
} from "./detail-sections";
|
||||
import { failedStatusLabel, jsonTargetLabel } from "./item-labels";
|
||||
|
||||
type UserMessageItem = Extract<TurnItem, { type: "userMessage" }>;
|
||||
type AgentMessageItem = Extract<TurnItem, { type: "agentMessage" }>;
|
||||
|
|
@ -63,18 +55,22 @@ interface MessageStreamTextData extends BaseStreamItemData {
|
|||
}
|
||||
|
||||
interface ToolMessageStreamData extends BaseStreamItemData {
|
||||
text: string;
|
||||
toolLabel?: string;
|
||||
text?: string;
|
||||
toolName?: string;
|
||||
primaryTarget?: MessageStreamPrimaryTarget;
|
||||
operation?: string;
|
||||
failureReason?: string;
|
||||
status?: string;
|
||||
output?: string;
|
||||
details?: MessageStreamDetailSection[];
|
||||
toolCall?: Extract<MessageStreamItem, { kind: "tool" }>["toolCall"];
|
||||
webSearch?: Extract<MessageStreamItem, { kind: "tool" }>["webSearch"];
|
||||
imageGeneration?: Extract<MessageStreamItem, { kind: "tool" }>["imageGeneration"];
|
||||
executionState?: ExecutionState;
|
||||
summaryPath?: boolean;
|
||||
}
|
||||
|
||||
interface CommandMessageStreamData extends BaseStreamItemData {
|
||||
actionLabel: string;
|
||||
text: string;
|
||||
commandAction: "read" | "search" | "listFiles" | "command";
|
||||
commandTarget: CommandMessageStreamTarget;
|
||||
command: string;
|
||||
cwd: string;
|
||||
status: string;
|
||||
|
|
@ -85,7 +81,6 @@ interface CommandMessageStreamData extends BaseStreamItemData {
|
|||
}
|
||||
|
||||
interface FileChangeMessageStreamData extends BaseStreamItemData {
|
||||
text: string;
|
||||
status: string;
|
||||
changes: MessageStreamFileChange[];
|
||||
executionState: ExecutionState;
|
||||
|
|
@ -304,17 +299,17 @@ function mcpToolCallMessageStreamItem(item: McpToolCallItem, turnId?: string): M
|
|||
function mcpToolCallMessageStreamItemDataFromItem(item: McpToolCallItem): ToolMessageStreamData {
|
||||
const name = `${item.server}.${item.tool}`;
|
||||
const target = jsonTargetLabel(item.arguments);
|
||||
const failure = item.error?.message ? truncate(item.error.message, 96) : failedStatusLabel(item.status);
|
||||
return {
|
||||
id: item.id,
|
||||
text: compactToolSummary(null, target, statusQualifier(item.status, failure)),
|
||||
toolLabel: name,
|
||||
toolName: name,
|
||||
...(target ? { primaryTarget: { kind: "value" as const, value: target } } : {}),
|
||||
...(item.error?.message ? { failureReason: item.error.message } : {}),
|
||||
status: item.status,
|
||||
details: jsonDetails([
|
||||
["Arguments JSON", item.arguments],
|
||||
["Result JSON", item.result],
|
||||
["Error JSON", item.error],
|
||||
]),
|
||||
toolCall: {
|
||||
arguments: item.arguments,
|
||||
result: item.result,
|
||||
error: item.error,
|
||||
},
|
||||
output: "",
|
||||
executionState: mcpToolCallExecutionState(item.status),
|
||||
};
|
||||
|
|
@ -325,13 +320,17 @@ function toolMessageStreamItemFromData(data: ToolMessageStreamData, turnId?: str
|
|||
id: data.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: data.text,
|
||||
...definedProp("toolLabel", data.toolLabel),
|
||||
...definedProp("summaryPath", data.summaryPath),
|
||||
...definedProp("text", data.text),
|
||||
...definedProp("toolName", data.toolName),
|
||||
...definedProp("primaryTarget", data.primaryTarget),
|
||||
...definedProp("operation", data.operation),
|
||||
...definedProp("failureReason", data.failureReason),
|
||||
...definedProp("turnId", turnId),
|
||||
sourceItemId: data.id,
|
||||
...definedProp("status", data.status),
|
||||
...definedProp("details", data.details),
|
||||
...definedProp("toolCall", data.toolCall),
|
||||
...definedProp("webSearch", data.webSearch),
|
||||
...definedProp("imageGeneration", data.imageGeneration),
|
||||
...definedProp("output", data.output),
|
||||
...("executionState" in data ? { executionState: data.executionState } : {}),
|
||||
};
|
||||
|
|
@ -347,13 +346,14 @@ function dynamicToolCallMessageStreamItemDataFromItem(item: DynamicToolCallItem)
|
|||
const failure = item.success === false ? "failed" : failedStatusLabel(item.status);
|
||||
return {
|
||||
id: item.id,
|
||||
text: compactToolSummary(null, target, statusQualifier(item.status, failure)),
|
||||
toolLabel: name,
|
||||
toolName: name,
|
||||
...(target ? { primaryTarget: { kind: "value" as const, value: target } } : {}),
|
||||
...(failure ? { failureReason: failure } : {}),
|
||||
status: item.status,
|
||||
details: jsonDetails([
|
||||
["Arguments JSON", item.arguments],
|
||||
["Result JSON", item.contentItems],
|
||||
]),
|
||||
toolCall: {
|
||||
arguments: item.arguments,
|
||||
result: item.contentItems,
|
||||
},
|
||||
output: "",
|
||||
executionState: dynamicToolCallExecutionState(item.status, item.success),
|
||||
};
|
||||
|
|
@ -366,9 +366,10 @@ function webSearchMessageStreamItem(item: WebSearchItem, turnId?: string): Messa
|
|||
function webSearchMessageStreamItemDataFromItem(item: WebSearchItem): ToolMessageStreamData {
|
||||
return {
|
||||
id: item.id,
|
||||
text: webSearchSummary(item),
|
||||
toolLabel: "web search",
|
||||
details: webSearchDetails(item),
|
||||
toolName: "web search",
|
||||
operation: item.action?.type ?? (item.query ? "search" : "webSearch"),
|
||||
...(webSearchTarget(item) ? { primaryTarget: { kind: "value" as const, value: webSearchTarget(item) ?? "" } } : {}),
|
||||
webSearch: webSearchDetails(item),
|
||||
output: "",
|
||||
};
|
||||
}
|
||||
|
|
@ -380,9 +381,8 @@ function imageViewMessageStreamItem(item: ImageViewItem, turnId?: string): Messa
|
|||
function imageViewMessageStreamItemDataFromItem(item: ImageViewItem): ToolMessageStreamData {
|
||||
return {
|
||||
id: item.id,
|
||||
text: compactToolSummary(null, item.path),
|
||||
toolLabel: "imageView",
|
||||
summaryPath: true,
|
||||
toolName: "imageView",
|
||||
primaryTarget: { kind: "path", path: item.path },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -392,17 +392,20 @@ function imageGenerationMessageStreamItem(item: ImageGenerationItem, turnId?: st
|
|||
|
||||
function imageGenerationMessageStreamItemDataFromItem(item: ImageGenerationItem): ToolMessageStreamData {
|
||||
const target = item.savedPath ?? item.result;
|
||||
const failureReason = failedStatusLabel(item.status);
|
||||
return {
|
||||
id: item.id,
|
||||
text: compactToolSummary(null, target, statusQualifier(item.status, failedStatusLabel(item.status))),
|
||||
toolLabel: "imageGeneration",
|
||||
summaryPath: Boolean(item.savedPath),
|
||||
toolName: "imageGeneration",
|
||||
...(target
|
||||
? { primaryTarget: item.savedPath ? { kind: "path" as const, path: item.savedPath } : { kind: "value" as const, value: target } }
|
||||
: {}),
|
||||
...(failureReason ? { failureReason } : {}),
|
||||
status: item.status,
|
||||
details: [
|
||||
...bodyDetail("Saved path", item.savedPath),
|
||||
...bodyDetail("Revised prompt", item.revisedPrompt),
|
||||
...bodyDetail("Result", item.result),
|
||||
],
|
||||
imageGeneration: {
|
||||
...definedProp("savedPath", item.savedPath),
|
||||
revisedPrompt: item.revisedPrompt,
|
||||
result: item.result,
|
||||
},
|
||||
output: "",
|
||||
executionState: imageGenerationExecutionState(item.status),
|
||||
};
|
||||
|
|
@ -415,8 +418,8 @@ function reviewModeMessageStreamItem(item: ReviewModeItem, turnId?: string): Mes
|
|||
function reviewModeMessageStreamItemDataFromItem(item: ReviewModeItem): ToolMessageStreamData {
|
||||
return {
|
||||
id: item.id,
|
||||
text: item.type === "enteredReviewMode" ? "Entered review mode" : "Exited review mode",
|
||||
toolLabel: item.type,
|
||||
toolName: item.type,
|
||||
primaryTarget: { kind: "value", value: item.type === "enteredReviewMode" ? "Entered review mode" : "Exited review mode" },
|
||||
output: item.review,
|
||||
};
|
||||
}
|
||||
|
|
@ -425,16 +428,15 @@ function contextCompactionMessageStreamItem(item: ContextCompactionItem, turnId?
|
|||
return contextCompactionMessageStreamItemFromData(contextCompactionMessageStreamItemDataFromItem(item), turnId);
|
||||
}
|
||||
|
||||
function contextCompactionMessageStreamItemDataFromItem(item: ContextCompactionItem): MessageStreamTextData {
|
||||
return { id: item.id, text: "Context compaction" };
|
||||
function contextCompactionMessageStreamItemDataFromItem(item: ContextCompactionItem): BaseStreamItemData {
|
||||
return { id: item.id };
|
||||
}
|
||||
|
||||
function contextCompactionMessageStreamItemFromData(data: MessageStreamTextData, turnId?: string): MessageStreamItem {
|
||||
function contextCompactionMessageStreamItemFromData(data: BaseStreamItemData, turnId?: string): MessageStreamItem {
|
||||
return {
|
||||
id: data.id,
|
||||
kind: "contextCompaction",
|
||||
role: "tool",
|
||||
text: data.text,
|
||||
...definedProp("turnId", turnId),
|
||||
sourceItemId: data.id,
|
||||
};
|
||||
|
|
@ -447,25 +449,29 @@ function reasoningText(item: ReasoningItem): string {
|
|||
.join("\n\n");
|
||||
}
|
||||
|
||||
function commandTargetLabel(item: CommandExecutionItem): string {
|
||||
function commandTarget(item: CommandExecutionItem): CommandMessageStreamTarget {
|
||||
const action = representativeCommandAction(item.commandActions);
|
||||
if (action?.type === "search") {
|
||||
const query = commandActionValue(action.query);
|
||||
const path = commandActionPathLabel(action.path, item.cwd);
|
||||
if (query && path) return `${quoteInline(query)} in ${path}`;
|
||||
if (query) return quoteInline(query);
|
||||
if (path) return path;
|
||||
const query = commandActionValue(action.query) ?? undefined;
|
||||
const path = commandActionValue(action.path) ?? undefined;
|
||||
return { kind: "search", ...(query ? { query } : {}), ...(path ? { path } : {}) };
|
||||
}
|
||||
if (action?.type === "read") return commandReadTargetLabel(action, item.cwd);
|
||||
if (action?.type === "listFiles") return commandActionPathLabel(action.path, item.cwd) ?? "workspace";
|
||||
return unwrapShellLoginCommand(firstCommandLine(item.command));
|
||||
if (action?.type === "read") {
|
||||
const path = commandActionValue(action.path) ?? undefined;
|
||||
return { kind: "read", name: action.name, ...(path ? { path } : {}) };
|
||||
}
|
||||
if (action?.type === "listFiles") {
|
||||
const path = commandActionValue(action.path) ?? undefined;
|
||||
return { kind: "listFiles", ...(path ? { path } : {}) };
|
||||
}
|
||||
return { kind: "command", commandLine: unwrapShellLoginCommand(firstCommandLine(item.command)) };
|
||||
}
|
||||
|
||||
function commandActionLabel(item: CommandExecutionItem): string {
|
||||
function commandActionKind(item: CommandExecutionItem): "read" | "search" | "listFiles" | "command" {
|
||||
const action = representativeCommandAction(item.commandActions);
|
||||
if (action?.type === "read") return "read";
|
||||
if (action?.type === "search") return "search";
|
||||
if (action?.type === "listFiles") return "list files";
|
||||
if (action?.type === "listFiles") return "listFiles";
|
||||
return "command";
|
||||
}
|
||||
|
||||
|
|
@ -484,17 +490,6 @@ function commandActionValue(value: string | null): string | null {
|
|||
return trimmed !== undefined && trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function commandActionPathLabel(value: string | null, cwd: string): string | null {
|
||||
const path = commandActionValue(value);
|
||||
return path ? pathRelativeToWorkspace(path, cwd) : null;
|
||||
}
|
||||
|
||||
function commandReadTargetLabel(action: Extract<CommandAction, { type: "read" }>, cwd: string): string {
|
||||
const path = commandActionValue(action.path);
|
||||
if (path) return pathRelativeToWorkspace(path, cwd);
|
||||
return action.name;
|
||||
}
|
||||
|
||||
function firstCommandLine(command: string): string {
|
||||
return (
|
||||
command
|
||||
|
|
@ -519,16 +514,6 @@ function unquoteShellCommand(value: string): string {
|
|||
return quote === "'" ? inner.replace(/'\\''/g, "'") : inner.replace(/\\(["\\$`])/g, "$1");
|
||||
}
|
||||
|
||||
function quoteInline(value: string): string {
|
||||
return value.includes(" ") ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
||||
function fileChangeTargetLabel(changes: MessageStreamFileChange[]): string {
|
||||
if (changes.length === 0) return "no files";
|
||||
if (changes.length === 1) return changes[0]?.path ?? "1 file";
|
||||
return `${String(changes.length)} files`;
|
||||
}
|
||||
|
||||
function webSearchTarget(item: WebSearchItem): string | null {
|
||||
if (item.action?.type === "openPage") return item.action.url;
|
||||
if (item.action?.type === "findInPage") return item.action.pattern ?? item.action.url;
|
||||
|
|
@ -536,12 +521,6 @@ function webSearchTarget(item: WebSearchItem): string | null {
|
|||
return item.query;
|
||||
}
|
||||
|
||||
function webSearchSummary(item: WebSearchItem): string {
|
||||
const actionType = item.action?.type ?? (item.query ? "search" : "web search");
|
||||
const label = webSearchActionLabel(actionType);
|
||||
return compactToolSummary(label, webSearchTarget(item));
|
||||
}
|
||||
|
||||
function webSearchActionLabel(actionType: string): string {
|
||||
if (actionType === "openPage") return "open page";
|
||||
if (actionType === "findInPage") return "find in page";
|
||||
|
|
@ -562,22 +541,22 @@ function webSearchQueryList(
|
|||
return unique.join("; ");
|
||||
}
|
||||
|
||||
function webSearchDetails(item: WebSearchItem): MessageStreamDetailSection[] {
|
||||
const rows: { key: string; value: string }[] = [];
|
||||
if (item.action) rows.push({ key: "action", value: webSearchActionLabel(item.action.type) });
|
||||
function webSearchDetails(item: WebSearchItem): Extract<MessageStreamItem, { kind: "tool" }>["webSearch"] {
|
||||
const details: NonNullable<Extract<MessageStreamItem, { kind: "tool" }>["webSearch"]> = {};
|
||||
if (item.action) details.action = webSearchActionLabel(item.action.type);
|
||||
if (item.action?.type === "search") {
|
||||
const queries = webSearchQueryList(item.action.query, item.action.queries, item.query);
|
||||
if (queries) rows.push({ key: "query", value: queries });
|
||||
if (queries) details.query = queries;
|
||||
} else if (item.action?.type === "openPage") {
|
||||
if (item.action.url) rows.push({ key: "url", value: item.action.url });
|
||||
if (item.action.url) details.url = item.action.url;
|
||||
} else if (item.action?.type === "findInPage") {
|
||||
if (item.action.pattern) rows.push({ key: "pattern", value: item.action.pattern });
|
||||
if (item.action.url) rows.push({ key: "url", value: item.action.url });
|
||||
if (item.action.pattern) details.pattern = item.action.pattern;
|
||||
if (item.action.url) details.url = item.action.url;
|
||||
} else if (item.query) {
|
||||
rows.push({ key: "query", value: item.query });
|
||||
details.query = item.query;
|
||||
}
|
||||
|
||||
return metaDetail("web search", rows);
|
||||
return Object.keys(details).length > 0 ? details : undefined;
|
||||
}
|
||||
|
||||
function commandMessageStreamItem(item: CommandExecutionItem, turnId?: string): MessageStreamItem {
|
||||
|
|
@ -587,15 +566,10 @@ function commandMessageStreamItem(item: CommandExecutionItem, turnId?: string):
|
|||
function commandMessageStreamItemDataFromItem(item: CommandExecutionItem): CommandMessageStreamData {
|
||||
const exitCode = typeof item.exitCode === "number" ? item.exitCode : undefined;
|
||||
const durationMs = typeof item.durationMs === "number" ? item.durationMs : undefined;
|
||||
const target = commandTargetLabel(item);
|
||||
const qualifier =
|
||||
typeof exitCode === "number" && exitCode !== 0
|
||||
? `exit ${String(exitCode)}`
|
||||
: statusQualifier(item.status, failedStatusLabel(item.status));
|
||||
return {
|
||||
id: item.id,
|
||||
actionLabel: commandActionLabel(item),
|
||||
text: compactToolSummary(null, target, qualifier),
|
||||
commandAction: commandActionKind(item),
|
||||
commandTarget: commandTarget(item),
|
||||
command: item.command,
|
||||
cwd: item.cwd,
|
||||
status: item.status,
|
||||
|
|
@ -611,8 +585,8 @@ function commandMessageStreamItemFromData(data: CommandMessageStreamData, turnId
|
|||
id: data.id,
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
actionLabel: data.actionLabel,
|
||||
text: data.text,
|
||||
commandAction: data.commandAction,
|
||||
commandTarget: data.commandTarget,
|
||||
...definedProp("turnId", turnId),
|
||||
sourceItemId: data.id,
|
||||
command: data.command,
|
||||
|
|
@ -631,10 +605,8 @@ function fileChangeMessageStreamItem(item: FileChangeItem, turnId?: string): Mes
|
|||
|
||||
function fileChangeMessageStreamItemDataFromItem(item: FileChangeItem): FileChangeMessageStreamData {
|
||||
const changes = normalizeFileChanges(item.changes);
|
||||
const qualifier = statusQualifier(item.status, failedStatusLabel(item.status));
|
||||
return {
|
||||
id: item.id,
|
||||
text: compactToolSummary(null, fileChangeTargetLabel(changes), qualifier),
|
||||
status: item.status,
|
||||
changes,
|
||||
executionState: patchApplyExecutionState(item.status),
|
||||
|
|
@ -646,7 +618,6 @@ function fileChangeMessageStreamItemFromData(data: FileChangeMessageStreamData,
|
|||
id: data.id,
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: data.text,
|
||||
...definedProp("turnId", turnId),
|
||||
sourceItemId: data.id,
|
||||
status: data.status,
|
||||
|
|
@ -667,10 +638,6 @@ export function shouldSuppressLifecycleItem(item: TurnItem): boolean {
|
|||
return item.type === "agentMessage" || item.type === "userMessage";
|
||||
}
|
||||
|
||||
function pathRelativeToWorkspace(path: string, workspaceRoot?: string | null): string {
|
||||
return pathRelativeToRoot(path, workspaceRoot);
|
||||
}
|
||||
|
||||
function assertNever(_item: never): null {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function goalChangeItem(id: string, previous: ThreadGoal | null, next: Th
|
|||
kind: "goal",
|
||||
role: "tool",
|
||||
text: goalEventSummary(message, objective),
|
||||
details: [{ rows: [{ key: "action", value: action }] }, ...(objective ? [{ title: "Objective", body: objective }] : [])],
|
||||
action,
|
||||
...(objective ? { objective } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { definedProp } from "../../../utils";
|
||||
import type { MessageStreamDetailMetaRow, MessageStreamDetailSection, HookMessageStreamItem } from "./items";
|
||||
import type { HookMessageStreamItem } from "./items";
|
||||
|
||||
interface MessageStreamHookRun {
|
||||
id: string;
|
||||
|
|
@ -12,25 +12,24 @@ interface MessageStreamHookRun {
|
|||
|
||||
export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: string | null, status: string): HookMessageStreamItem | null {
|
||||
if (run.id.length === 0) return null;
|
||||
const entries = run.entries.map((entry) => `${entry.kind}: ${entry.text}`).join("\n");
|
||||
const metaRows: MessageStreamDetailMetaRow[] = [
|
||||
{ key: "status", value: status },
|
||||
{ key: "event", value: hookEventName(run.eventName) },
|
||||
...(run.statusMessage ? [{ key: "message", value: run.statusMessage }] : []),
|
||||
...(run.durationMs !== null ? [{ key: "duration", value: `${String(run.durationMs)}ms` }] : []),
|
||||
];
|
||||
const details: MessageStreamDetailSection[] = [{ rows: metaRows }, ...(entries ? [{ title: "Hook output", body: entries }] : [])];
|
||||
const eventName = hookEventName(run.eventName);
|
||||
const displayId = hookRunDisplayId(run);
|
||||
return {
|
||||
id: displayId,
|
||||
kind: "hook",
|
||||
role: "tool",
|
||||
text: hookSummary(run.eventName, run.statusMessage),
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
operation: eventName,
|
||||
...(run.statusMessage ? { primaryTarget: { kind: "value" as const, value: run.statusMessage } } : {}),
|
||||
...definedProp("turnId", turnId),
|
||||
sourceItemId: displayId,
|
||||
status,
|
||||
details,
|
||||
hookRun: {
|
||||
eventName,
|
||||
...definedProp("statusMessage", run.statusMessage ?? undefined),
|
||||
...(run.durationMs !== null ? { durationMs: `${String(run.durationMs)}ms` } : {}),
|
||||
entries: run.entries,
|
||||
},
|
||||
output: "",
|
||||
};
|
||||
}
|
||||
|
|
@ -43,9 +42,3 @@ function hookEventName(eventName: string | null | undefined): string {
|
|||
const trimmed = eventName?.trim();
|
||||
return trimmed && trimmed.length > 0 ? trimmed : "Hook";
|
||||
}
|
||||
|
||||
function hookSummary(eventName: string | null | undefined, statusMessage: string | null | undefined): string {
|
||||
const message = statusMessage?.trim();
|
||||
const event = hookEventName(eventName);
|
||||
return message ? `${event}: ${message}` : event;
|
||||
}
|
||||
|
|
|
|||
49
src/features/chat/message-stream/item-labels.ts
Normal file
49
src/features/chat/message-stream/item-labels.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export function failedStatusLabel(status: unknown): string | null {
|
||||
if (status === "failed") return "failed";
|
||||
if (status === "declined") return "declined";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function jsonTargetLabel(value: unknown): string | null {
|
||||
const direct = jsonTargetPrimitive(value);
|
||||
if (direct) return direct;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const priorityKeys = [
|
||||
"q",
|
||||
"query",
|
||||
"search_query",
|
||||
"url",
|
||||
"ref_id",
|
||||
"path",
|
||||
"file",
|
||||
"filename",
|
||||
"ticker",
|
||||
"location",
|
||||
"team",
|
||||
"league",
|
||||
"id",
|
||||
"target",
|
||||
"command",
|
||||
];
|
||||
|
||||
for (const key of priorityKeys) {
|
||||
const target = jsonTargetPrimitive(record[key]);
|
||||
if (target) return target;
|
||||
}
|
||||
|
||||
const firstEntry = Object.entries(record).find(([, entryValue]) => jsonTargetPrimitive(entryValue));
|
||||
return firstEntry ? jsonTargetPrimitive(firstEntry[1]) : null;
|
||||
}
|
||||
|
||||
function jsonTargetPrimitive(value: unknown): string | null {
|
||||
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (!Array.isArray(value)) return null;
|
||||
for (const item of value) {
|
||||
const target = jsonTargetLabel(item);
|
||||
if (target) return target;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -23,26 +23,100 @@ interface MessageStreamBase {
|
|||
id: string;
|
||||
kind: MessageStreamItemKind;
|
||||
role: MessageStreamRole;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
sourceItemId?: string;
|
||||
executionState?: ExecutionState;
|
||||
}
|
||||
|
||||
export interface MessageStreamDetailMetaRow {
|
||||
export type MessageStreamPrimaryTarget =
|
||||
| {
|
||||
kind: "path";
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
kind: "value";
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface MessageStreamToolCallDetails {
|
||||
arguments?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
interface MessageStreamWebSearchDetails {
|
||||
action?: string;
|
||||
query?: string;
|
||||
url?: string;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamImageGenerationDetails {
|
||||
savedPath?: string;
|
||||
revisedPrompt?: string | null;
|
||||
result?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamHookRunDetails {
|
||||
eventName: string;
|
||||
statusMessage?: string;
|
||||
durationMs?: string;
|
||||
entries: readonly { kind: string; text: string }[];
|
||||
}
|
||||
|
||||
export interface MessageStreamAuditFact {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamDetailSection {
|
||||
export interface MessageStreamNoticeSection {
|
||||
title?: string;
|
||||
auditFacts?: MessageStreamAuditFact[];
|
||||
body?: string;
|
||||
rows?: MessageStreamDetailMetaRow[];
|
||||
}
|
||||
|
||||
interface MessageStreamApprovalResultDetails {
|
||||
status: string;
|
||||
scope: "session" | "turn";
|
||||
request: string;
|
||||
auditFacts: MessageStreamAuditFact[];
|
||||
}
|
||||
|
||||
export interface MessageStreamUserInputQuestionResult {
|
||||
id: string;
|
||||
header: string;
|
||||
question: string;
|
||||
answer?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamReviewResultDetails {
|
||||
auditFacts: MessageStreamAuditFact[];
|
||||
}
|
||||
|
||||
export type CommandMessageStreamTarget =
|
||||
| {
|
||||
kind: "read";
|
||||
path?: string;
|
||||
name: string;
|
||||
}
|
||||
| {
|
||||
kind: "search";
|
||||
query?: string;
|
||||
path?: string;
|
||||
}
|
||||
| {
|
||||
kind: "listFiles";
|
||||
path?: string;
|
||||
}
|
||||
| {
|
||||
kind: "command";
|
||||
commandLine: string;
|
||||
};
|
||||
|
||||
interface MessageStreamMessageBase extends MessageStreamBase {
|
||||
kind: "message";
|
||||
role: "user" | "assistant";
|
||||
text: string;
|
||||
clientId?: string;
|
||||
copyText?: string;
|
||||
referencedThread?: ReferencedThreadMetadata;
|
||||
|
|
@ -79,38 +153,44 @@ export interface MessageStreamFileMention {
|
|||
interface SystemMessageStreamItem extends MessageStreamBase {
|
||||
kind: "system";
|
||||
role: "system";
|
||||
details?: MessageStreamDetailSection[];
|
||||
text: string;
|
||||
noticeSections?: MessageStreamNoticeSection[];
|
||||
}
|
||||
|
||||
export interface GoalMessageStreamItem extends MessageStreamBase {
|
||||
kind: "goal";
|
||||
role: "tool";
|
||||
text: string;
|
||||
action: string;
|
||||
objective?: string;
|
||||
details?: MessageStreamDetailSection[];
|
||||
}
|
||||
|
||||
interface UserInputResultMessageStreamItem extends MessageStreamBase {
|
||||
kind: "userInputResult";
|
||||
role: "tool";
|
||||
details?: MessageStreamDetailSection[];
|
||||
text: string;
|
||||
questions: MessageStreamUserInputQuestionResult[];
|
||||
}
|
||||
|
||||
export interface ApprovalResultMessageStreamItem extends MessageStreamBase {
|
||||
kind: "approvalResult";
|
||||
role: "tool";
|
||||
details?: MessageStreamDetailSection[];
|
||||
text: string;
|
||||
approval: MessageStreamApprovalResultDetails;
|
||||
}
|
||||
|
||||
export interface ReviewResultMessageStreamItem extends MessageStreamBase {
|
||||
kind: "reviewResult";
|
||||
role: "tool";
|
||||
details?: MessageStreamDetailSection[];
|
||||
text: string;
|
||||
review?: MessageStreamReviewResultDetails;
|
||||
}
|
||||
|
||||
export interface CommandMessageStreamItem extends MessageStreamBase {
|
||||
kind: "command";
|
||||
role: "tool";
|
||||
actionLabel?: string;
|
||||
commandAction: "read" | "search" | "listFiles" | "command";
|
||||
commandTarget: CommandMessageStreamTarget;
|
||||
command: string;
|
||||
cwd: string;
|
||||
status: string;
|
||||
|
|
@ -135,24 +215,31 @@ export interface FileChangeMessageStreamItem extends MessageStreamBase {
|
|||
|
||||
interface ToolMessageStreamBase extends MessageStreamBase {
|
||||
role: "tool";
|
||||
text?: string;
|
||||
activityKind?: "userSteered";
|
||||
toolLabel?: string;
|
||||
summaryPath?: boolean;
|
||||
toolName?: string;
|
||||
primaryTarget?: MessageStreamPrimaryTarget;
|
||||
operation?: string;
|
||||
failureReason?: string;
|
||||
status?: string;
|
||||
output?: string;
|
||||
details?: MessageStreamDetailSection[];
|
||||
}
|
||||
|
||||
export interface ToolCallMessageStreamItem extends ToolMessageStreamBase {
|
||||
kind: "tool";
|
||||
toolCall?: MessageStreamToolCallDetails;
|
||||
webSearch?: MessageStreamWebSearchDetails;
|
||||
imageGeneration?: MessageStreamImageGenerationDetails;
|
||||
}
|
||||
|
||||
export interface HookMessageStreamItem extends ToolMessageStreamBase {
|
||||
kind: "hook";
|
||||
hookRun?: MessageStreamHookRunDetails;
|
||||
}
|
||||
|
||||
export interface ReasoningMessageStreamItem extends ToolMessageStreamBase {
|
||||
kind: "reasoning";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ContextCompactionMessageStreamItem extends MessageStreamBase {
|
||||
|
|
@ -168,6 +255,7 @@ interface TaskProgressStep {
|
|||
export interface TaskProgressMessageStreamItem extends MessageStreamBase {
|
||||
kind: "taskProgress";
|
||||
role: "tool";
|
||||
text?: string;
|
||||
explanation: string | null;
|
||||
steps: TaskProgressStep[];
|
||||
status: string;
|
||||
|
|
@ -188,6 +276,7 @@ export interface AgentRunSummaryAgent {
|
|||
export interface AgentMessageStreamItem extends MessageStreamBase {
|
||||
kind: "agent";
|
||||
role: "tool";
|
||||
text?: string;
|
||||
tool: string;
|
||||
status: string;
|
||||
senderThreadId: string;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function steeringActivityItem(item: TimelineItem, turnId: string): MessageStream
|
|||
turnId,
|
||||
...(item.sourceItemId ? { sourceItemId: item.sourceItemId } : {}),
|
||||
activityKind: STEERING_ACTIVITY_KIND,
|
||||
toolLabel: STEERING_ACTIVITY_LABEL,
|
||||
toolName: STEERING_ACTIVITY_LABEL,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MessageStreamItem, ExecutionState } from "./items";
|
||||
import type { MessageStreamAuditFact, MessageStreamItem, ExecutionState } from "./items";
|
||||
import { pathsRelativeToRoot } from "./path-labels";
|
||||
import { permissionRows } from "./permission-rows";
|
||||
|
||||
|
|
@ -52,11 +52,6 @@ type AutoReviewAction =
|
|||
}
|
||||
| { type: "requestPermissions"; reason: string | null; permissions: Parameters<typeof permissionRows>[0] };
|
||||
|
||||
interface MessageStreamDetailRow {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function createReviewResultItem(id: string, text: string): MessageStreamItem {
|
||||
const parsed = parseAutomaticApprovalReviewMessage(text);
|
||||
if (parsed) {
|
||||
|
|
@ -66,7 +61,7 @@ export function createReviewResultItem(id: string, text: string): MessageStreamI
|
|||
role: "tool",
|
||||
text: parsed.summary,
|
||||
executionState: autoReviewExecutionState(parsed.status),
|
||||
details: [{ title: "Review", rows: parsed.rows }],
|
||||
review: { auditFacts: parsed.rows },
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
|
@ -98,7 +93,7 @@ export function createAutoReviewResultItem(params: AutoReviewNotification): Mess
|
|||
text,
|
||||
turnId: params.turnId,
|
||||
executionState: completed ? autoReviewExecutionState(status) : "running",
|
||||
details: [{ title: "Review", rows }],
|
||||
review: { auditFacts: rows },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +123,7 @@ function parseAutomaticApprovalReviewMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function autoReviewActionRows(action: AutoReviewAction): MessageStreamDetailRow[] {
|
||||
function autoReviewActionRows(action: AutoReviewAction): MessageStreamAuditFact[] {
|
||||
if (action.type === "command") {
|
||||
return [
|
||||
{ key: "action", value: "command" },
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { FileUpdateChange } from "../../../app-server/protocol/turn";
|
|||
import type { MessageStreamItem, MessageStreamItemKind } from "./items";
|
||||
import { normalizeFileChanges } from "./from-turn-items";
|
||||
|
||||
const STREAMED_TOOL_DETAILS_TEXT = "details";
|
||||
export const STREAMED_COMMAND_RUNNING_TEXT = "Command running";
|
||||
export const STREAMED_FILE_CHANGE_IN_PROGRESS_TEXT = "File change inProgress";
|
||||
export const STREAMED_MCP_PROGRESS_LABEL = "mcp progress";
|
||||
|
|
@ -35,8 +34,7 @@ export function streamedToolOutputMessageStreamItem(params: {
|
|||
id: params.id,
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: STREAMED_TOOL_DETAILS_TEXT,
|
||||
toolLabel: params.fallbackLabel,
|
||||
toolName: params.fallbackLabel,
|
||||
turnId: params.turnId,
|
||||
sourceItemId: params.id,
|
||||
output: params.output,
|
||||
|
|
@ -54,7 +52,6 @@ export function streamedItemOutputMessageStreamItem(params: {
|
|||
id: params.id,
|
||||
kind: params.kind,
|
||||
role: "tool",
|
||||
text: params.fallbackText,
|
||||
turnId: params.turnId,
|
||||
sourceItemId: params.id,
|
||||
output: params.output,
|
||||
|
|
@ -65,6 +62,8 @@ export function streamedItemOutputMessageStreamItem(params: {
|
|||
executionState: "running",
|
||||
}
|
||||
: {
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: params.fallbackText },
|
||||
command: params.fallbackText,
|
||||
cwd: UNKNOWN_STREAMED_COMMAND_CWD,
|
||||
status: "running",
|
||||
|
|
@ -83,7 +82,6 @@ export function streamingFileChangeMessageStreamItem(
|
|||
id: itemId,
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: `File change ${status}`,
|
||||
turnId,
|
||||
sourceItemId: itemId,
|
||||
status,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MessageStreamDetailSection, MessageStreamItem } from "./items";
|
||||
import type { MessageStreamItem, MessageStreamNoticeSection } from "./items";
|
||||
|
||||
export function createSystemItem(id: string, text: string): MessageStreamItem {
|
||||
return {
|
||||
|
|
@ -9,12 +9,12 @@ export function createSystemItem(id: string, text: string): MessageStreamItem {
|
|||
};
|
||||
}
|
||||
|
||||
export function createStructuredSystemItem(id: string, text: string, details: MessageStreamDetailSection[]): MessageStreamItem {
|
||||
export function createStructuredSystemItem(id: string, text: string, noticeSections: MessageStreamNoticeSection[]): MessageStreamItem {
|
||||
return {
|
||||
id,
|
||||
kind: "system",
|
||||
role: "system",
|
||||
text,
|
||||
details,
|
||||
noticeSections,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,11 @@ export function taskProgressMessageStreamItem(
|
|||
plan: readonly TaskPlanStep[],
|
||||
): MessageStreamItem {
|
||||
const trimmedExplanation = explanation?.trim();
|
||||
const lines = plan.map((step) => `${taskProgressTextMarker(step.status)} ${step.step}`);
|
||||
const body = [trimmedExplanation, ...lines].filter((line): line is string => Boolean(line && line.length > 0)).join("\n");
|
||||
const status = plan.some((step) => step.status === "inProgress" || step.status === "pending") ? "inProgress" : "completed";
|
||||
return {
|
||||
id: `plan-progress-${turnId}`,
|
||||
kind: "taskProgress",
|
||||
role: "tool",
|
||||
text: body.length > 0 ? body : "Plan updated",
|
||||
turnId,
|
||||
sourceItemId: `plan-progress-${turnId}`,
|
||||
explanation: trimmedExplanation !== undefined && trimmedExplanation.length > 0 ? trimmedExplanation : null,
|
||||
|
|
@ -43,12 +40,6 @@ export function taskProgressMessageStreamItem(
|
|||
};
|
||||
}
|
||||
|
||||
function taskProgressTextMarker(status: TaskStepStatus): string {
|
||||
if (status === "completed") return "[x]";
|
||||
if (status === "inProgress") return "[>]";
|
||||
return "[ ]";
|
||||
}
|
||||
|
||||
function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState {
|
||||
return states[status] ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,16 +31,19 @@ export function timelineItemFromMessageStreamItem(
|
|||
detailShape: detailShapeForMessageStreamItem(item, semanticKind),
|
||||
renderSurface: renderSurfaceForMessageStreamItem(item),
|
||||
lifecycle: lifecycleForMessageStreamItem(item),
|
||||
text: item.text,
|
||||
text: timelineTextForMessageStreamItem(item),
|
||||
...definedProp("copyText", copyText),
|
||||
actions,
|
||||
streamItem: item,
|
||||
};
|
||||
if ("details" in item) return { ...base, ...definedProp("details", item.details) } as TimelineItem;
|
||||
if (item.kind === "fileChange") return { ...base, changes: item.changes } as TimelineItem;
|
||||
return base as TimelineItem;
|
||||
}
|
||||
|
||||
function timelineTextForMessageStreamItem(item: MessageStreamItem): string {
|
||||
return "text" in item && typeof item.text === "string" ? item.text : "";
|
||||
}
|
||||
|
||||
export function timelineActionsForMessageStreamItem(
|
||||
item: MessageStreamItem,
|
||||
semanticKind = semanticKindForMessageStreamItem(item),
|
||||
|
|
@ -141,12 +144,17 @@ function detailShapeForMessageStreamItem(item: MessageStreamItem, semanticKind:
|
|||
return "eventSummary";
|
||||
case "tool":
|
||||
case "hook":
|
||||
return item.details && item.details.length > 0 ? "jsonAudit" : "plainText";
|
||||
return hasGenericToolDetails(item) ? "jsonAudit" : "plainText";
|
||||
case "reasoning":
|
||||
return "plainText";
|
||||
}
|
||||
}
|
||||
|
||||
function hasGenericToolDetails(item: Extract<MessageStreamItem, { kind: "tool" | "hook" }>): boolean {
|
||||
if (item.kind === "hook") return Boolean(item.hookRun);
|
||||
return Boolean(item.toolCall ?? item.webSearch ?? item.imageGeneration);
|
||||
}
|
||||
|
||||
function renderSurfaceForMessageStreamItem(item: MessageStreamItem): TimelineRenderSurface {
|
||||
if (item.kind === "message" || item.kind === "system" || item.kind === "userInputResult") return "textMessage";
|
||||
if (item.kind === "taskProgress" || item.kind === "agent" || item.kind === "reasoning" || item.kind === "contextCompaction") {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type {
|
|||
ApprovalResultMessageStreamItem,
|
||||
CommandMessageStreamItem,
|
||||
ContextCompactionMessageStreamItem,
|
||||
MessageStreamDetailSection,
|
||||
MessageStreamFileChange,
|
||||
MessageStreamItem,
|
||||
ExecutionState,
|
||||
|
|
@ -90,7 +89,6 @@ interface TimelineToolResultItem extends TimelineBaseItem<
|
|||
> {
|
||||
detailShape: "commandAudit" | "diffSet" | "jsonAudit" | "eventSummary" | "plainText";
|
||||
renderSurface: "toolResult";
|
||||
details?: readonly MessageStreamDetailSection[];
|
||||
changes?: readonly MessageStreamFileChange[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { McpServerStartupStatus } from "../../../../domain/server/diagnosti
|
|||
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
|
||||
import { classifyAppServerLog } from "./app-server-logs";
|
||||
import { activeTurnId, type ChatAction, type ChatState, type ChatStateStore } from "../../state/reducer";
|
||||
import type { MessageStreamDetailSection } from "../../message-stream/items";
|
||||
import type { MessageStreamNoticeSection } from "../../message-stream/items";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../../message-stream/system-items";
|
||||
import { approvalResponse, type ApprovalAction, type PendingApproval } from "../server-requests/approval";
|
||||
import { userInputResponse, type PendingUserInput } from "../server-requests/user-input";
|
||||
|
|
@ -117,7 +117,7 @@ export class ChatInboundController {
|
|||
this.dispatch({ type: "message-stream/system-item-added", item: createSystemItem(this.localItemId("system"), text) });
|
||||
}
|
||||
|
||||
addStructuredSystemMessage(text: string, details: MessageStreamDetailSection[]): void {
|
||||
addStructuredSystemMessage(text: string, details: MessageStreamNoticeSection[]): void {
|
||||
this.dispatch({
|
||||
type: "message-stream/system-item-added",
|
||||
item: createStructuredSystemItem(this.localItemId("system"), text, details),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import type { ChatComposerController } from "./conversation/composer/controller"
|
|||
import { createConversationParts } from "./conversation/composition";
|
||||
import type { ComposerSubmitActions } from "./conversation/turns/composer-submit-actions";
|
||||
import { codexPanelDisplayTitle } from "./threads/title-display";
|
||||
import type { MessageStreamDetailSection, MessageStreamItem } from "./message-stream/items";
|
||||
import type { MessageStreamItem, MessageStreamNoticeSection } from "./message-stream/items";
|
||||
import { createStructuredSystemItem, createSystemItem } from "./message-stream/system-items";
|
||||
import {
|
||||
effortStatusLines as buildEffortStatusLines,
|
||||
|
|
@ -146,7 +146,7 @@ interface ChatSessionSideEffects {
|
|||
status: {
|
||||
set: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamDetailSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
};
|
||||
composer: {
|
||||
setText: (text: string) => void;
|
||||
|
|
@ -931,14 +931,14 @@ export class ChatPanelSession {
|
|||
});
|
||||
}
|
||||
|
||||
private connectionDiagnosticDetails(): MessageStreamDetailSection[] {
|
||||
private connectionDiagnosticDetails(): MessageStreamNoticeSection[] {
|
||||
return connectionDiagnosticsModel({
|
||||
state: this.state,
|
||||
connected: this.parts.connection.manager.isConnected(),
|
||||
configuredCommand: this.environment.plugin.settings.codexPath,
|
||||
}).map((section) => ({
|
||||
title: section.title,
|
||||
rows: section.rows.map((row) => ({ key: row.label, value: row.value })),
|
||||
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -954,7 +954,7 @@ export class ChatPanelSession {
|
|||
return createSystemItem(`system-${String(Date.now())}-${Math.random().toString(36).slice(2)}`, text);
|
||||
}
|
||||
|
||||
private structuredSystemItem(text: string, details: MessageStreamDetailSection[]): MessageStreamItem {
|
||||
private structuredSystemItem(text: string, details: MessageStreamNoticeSection[]): MessageStreamItem {
|
||||
return createStructuredSystemItem(`system-${String(Date.now())}-${Math.random().toString(36).slice(2)}`, text, details);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export function appendItemText(
|
|||
): MessageStreamItem[] {
|
||||
const index = items.findIndex((item) => item.sourceItemId === sourceItemId);
|
||||
if (index !== -1) {
|
||||
return items.map((item, itemIndex) => (itemIndex === index ? { ...item, text: `${item.text}${delta}` } : item));
|
||||
return items.map((item, itemIndex) => (itemIndex === index ? { ...item, text: `${"text" in item ? item.text : ""}${delta}` } : item));
|
||||
}
|
||||
return [...items, streamedTextMessageStreamItem({ id: sourceItemId, kind, label, delta, turnId })];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export function messageStreamRollbackCandidate(
|
|||
return {
|
||||
turnId: lastTurnId,
|
||||
itemId: userMessage.id,
|
||||
text: userMessage.text,
|
||||
text: userMessage.copyText ?? userMessage.text,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +302,7 @@ function appendItemTextToMessageStream(
|
|||
return updateActiveSegment(state, turnId, (segment) => {
|
||||
const index = segment.indexBySourceItemId.get(sourceItemId);
|
||||
if (index !== undefined) {
|
||||
return replaceActiveSegmentItem(segment, index, (item) => ({ ...item, text: `${item.text}${delta}` }));
|
||||
return replaceActiveSegmentItem(segment, index, (item) => ({ ...item, text: `${"text" in item ? item.text : ""}${delta}` }));
|
||||
}
|
||||
return appendActiveSegmentItem(segment, {
|
||||
...streamedTextMessageStreamItem({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ThreadTitleContext } from "../../thread-title/model";
|
||||
import { truncate } from "../../../utils";
|
||||
import { isCompletedTurnOutcomeMessage } from "../message-stream/selectors";
|
||||
import type { MessageStreamItem } from "../message-stream/items";
|
||||
import type { MessageStreamItem, MessageStreamMessageItem } from "../message-stream/items";
|
||||
|
||||
const MAX_CONTEXT_CHARS = 4_000;
|
||||
|
||||
|
|
@ -11,7 +11,11 @@ export function threadTitleContextFromMessageStreamItems(turnId: string, items:
|
|||
turnItems.find((item) => item.kind === "message" && item.role === "user")?.text.trim() ??
|
||||
precedingUnscopedTitleSeed(turnId, items) ??
|
||||
"";
|
||||
const assistantResponse = [...turnItems].reverse().find(isCompletedTurnOutcomeMessage)?.text.trim() ?? "";
|
||||
const assistantResponse =
|
||||
[...turnItems]
|
||||
.reverse()
|
||||
.find((item): item is MessageStreamMessageItem => item.kind === "message" && isCompletedTurnOutcomeMessage(item))
|
||||
?.text.trim() ?? "";
|
||||
if (!userRequest || !assistantResponse) return null;
|
||||
return {
|
||||
userRequest: truncateForPrompt(userRequest),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Fragment, type ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { MessageStreamItemAnnotations } from "../../message-stream/layout";
|
||||
import type { MessageStreamDetailSection, MessageStreamItem } from "../../message-stream/items";
|
||||
import type { MessageStreamItem, MessageStreamNoticeSection, MessageStreamUserInputQuestionResult } from "../../message-stream/items";
|
||||
import { IconButton } from "../../../../shared/ui/components";
|
||||
import type { TextItemDetailStateContext, TextItemMetadataContext } from "./context";
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ export function TextItemDetails({
|
|||
context,
|
||||
}: {
|
||||
itemId: string;
|
||||
details: MessageStreamDetailSection[];
|
||||
details: TextItemDetailSection[];
|
||||
context: TextItemDetailStateContext;
|
||||
}): UiNode {
|
||||
return (
|
||||
|
|
@ -145,25 +145,49 @@ export function TextItemDetails({
|
|||
);
|
||||
}
|
||||
|
||||
export function SystemDetails({ details }: { details: MessageStreamDetailSection[] }): UiNode {
|
||||
export function SystemDetails({ details }: { details: MessageStreamNoticeSection[] }): UiNode {
|
||||
return (
|
||||
<>
|
||||
{details.map((section, index) => (
|
||||
<div key={`${section.title ?? ""}:${String(index)}`} className="codex-panel__output codex-panel__system-result-section">
|
||||
{section.title ? <div className="codex-panel__output-title">{section.title}</div> : null}
|
||||
<DetailSectionBody section={section} />
|
||||
<DetailSectionBody section={noticeDetailSection(section)} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailSectionBody({ section }: { section: MessageStreamDetailSection }): UiNode {
|
||||
export function userInputQuestionDetails(questions: readonly MessageStreamUserInputQuestionResult[]): TextItemDetailSection[] {
|
||||
return questions.map((question) => ({
|
||||
title: `Question: ${question.header}`,
|
||||
facts: [
|
||||
{ key: "Prompt", value: question.question },
|
||||
...(question.answer !== undefined ? [{ key: "Answer", value: question.answer }] : []),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
interface TextItemDetailSection {
|
||||
title?: string;
|
||||
facts?: { key: string; value: string }[];
|
||||
body?: string;
|
||||
}
|
||||
|
||||
function noticeDetailSection(section: MessageStreamNoticeSection): TextItemDetailSection {
|
||||
return {
|
||||
...(section.title !== undefined ? { title: section.title } : {}),
|
||||
...(section.auditFacts !== undefined ? { facts: section.auditFacts } : {}),
|
||||
...(section.body !== undefined ? { body: section.body } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function DetailSectionBody({ section }: { section: TextItemDetailSection }): UiNode {
|
||||
return (
|
||||
<>
|
||||
{section.rows && section.rows.length > 0 ? (
|
||||
{section.facts && section.facts.length > 0 ? (
|
||||
<dl className="codex-panel__meta-grid">
|
||||
{section.rows.map((row) => (
|
||||
{section.facts.map((row) => (
|
||||
<Fragment key={`${row.key}\n${row.value}`}>
|
||||
<dt>{row.key}</dt>
|
||||
<dd>{row.value}</dd>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ import { timelineItemFromMessageStreamItem } from "../../message-stream/timeline
|
|||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "./content-events";
|
||||
import type { TextItemContentContext, TextItemContext, TextMessageStreamItem } from "./context";
|
||||
import { TextItemHeader } from "./text-item-actions";
|
||||
import { AutoReviewSummaries, EditedFiles, MentionedFiles, TextItemDetails, ReferencedThread, SystemDetails } from "./text-item-metadata";
|
||||
import {
|
||||
AutoReviewSummaries,
|
||||
EditedFiles,
|
||||
MentionedFiles,
|
||||
TextItemDetails,
|
||||
ReferencedThread,
|
||||
SystemDetails,
|
||||
userInputQuestionDetails,
|
||||
} from "./text-item-metadata";
|
||||
|
||||
const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360;
|
||||
|
||||
|
|
@ -25,7 +33,6 @@ function TextItem({
|
|||
annotations?: MessageStreamItemAnnotations;
|
||||
}): UiNode {
|
||||
const collapsible = isCollapsibleUserMessage(item);
|
||||
const details = "details" in item ? item.details : undefined;
|
||||
const editedFiles = annotations?.editedFiles ?? [];
|
||||
const autoReviewSummaries = annotations?.autoReviewSummaries ?? [];
|
||||
return (
|
||||
|
|
@ -44,10 +51,10 @@ function TextItem({
|
|||
<MentionedFiles item={item} context={context} />
|
||||
) : null}
|
||||
{item.kind === "message" && autoReviewSummaries.length > 0 ? <AutoReviewSummaries summaries={autoReviewSummaries} /> : null}
|
||||
{item.kind === "system" && item.details && item.details.length > 0 ? (
|
||||
<SystemDetails details={item.details} />
|
||||
) : details && details.length > 0 ? (
|
||||
<TextItemDetails itemId={item.id} details={details} context={context} />
|
||||
{item.kind === "system" && item.noticeSections && item.noticeSections.length > 0 ? (
|
||||
<SystemDetails details={item.noticeSections} />
|
||||
) : item.kind === "userInputResult" && item.questions.length > 0 ? (
|
||||
<TextItemDetails itemId={item.id} details={userInputQuestionDetails(item.questions)} context={context} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { definedProp } from "../../../../utils";
|
||||
import { jsonPreview, truncate } from "../../../../utils";
|
||||
import { pathRelativeToRoot } from "../../message-stream/path-labels";
|
||||
import type {
|
||||
ApprovalResultMessageStreamItem,
|
||||
CommandMessageStreamItem,
|
||||
MessageStreamDetailSection,
|
||||
CommandMessageStreamTarget,
|
||||
MessageStreamFileChange,
|
||||
MessageStreamPrimaryTarget,
|
||||
MessageStreamItem,
|
||||
ExecutionState,
|
||||
FileChangeMessageStreamItem,
|
||||
|
|
@ -61,7 +62,14 @@ function commandToolView(item: CommandMessageStreamItem): ToolResultView {
|
|||
},
|
||||
...outputSection("Output", item.output),
|
||||
];
|
||||
return toolView(item, "codex-panel__tool-item", item.actionLabel ?? "command", `${item.id}:command-details`, details);
|
||||
return toolView(
|
||||
item,
|
||||
"codex-panel__tool-item",
|
||||
commandActionLabel(item.commandAction),
|
||||
`${item.id}:command-details`,
|
||||
details,
|
||||
commandSummary(item),
|
||||
);
|
||||
}
|
||||
|
||||
function fileChangeToolView(item: FileChangeMessageStreamItem, workspaceRoot?: string | null): ToolResultView {
|
||||
|
|
@ -95,23 +103,17 @@ function fileChangeToolView(item: FileChangeMessageStreamItem, workspaceRoot?: s
|
|||
}
|
||||
|
||||
function goalToolView(item: GoalMessageStreamItem): ToolResultView {
|
||||
return toolView(
|
||||
item,
|
||||
"codex-panel__tool-item codex-panel__tool-item--goal",
|
||||
"goal",
|
||||
`${item.id}:goal-details`,
|
||||
(item.details ?? []).flatMap(detailSection),
|
||||
);
|
||||
return toolView(item, "codex-panel__tool-item codex-panel__tool-item--goal", "goal", `${item.id}:goal-details`, goalDetails(item));
|
||||
}
|
||||
|
||||
function genericToolView(item: ToolCallMessageStreamItem | HookMessageStreamItem, workspaceRoot?: string | null): ToolResultView {
|
||||
return toolView(
|
||||
item,
|
||||
`codex-panel__tool-item codex-panel__tool-item--${item.kind}`,
|
||||
item.toolLabel ?? item.kind,
|
||||
item.toolName ?? item.kind,
|
||||
`${item.id}:details`,
|
||||
[...(item.details ?? []).flatMap(detailSection), ...outputSection(item.kind === "hook" ? "Hook output" : "Output", item.output)],
|
||||
item.summaryPath ? pathRelativeToRoot(item.text, workspaceRoot) : item.text,
|
||||
[...genericToolDetails(item), ...outputSection(item.kind === "hook" ? "Hook output" : "Output", item.output)],
|
||||
genericToolSummary(item, workspaceRoot),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +141,7 @@ function resultToolView(
|
|||
detailsKey: string,
|
||||
className: string,
|
||||
): ToolResultView {
|
||||
return toolView(item, `codex-panel__tool-item ${className}`, label, detailsKey, (item.details ?? []).flatMap(resultDetailSection));
|
||||
return toolView(item, `codex-panel__tool-item ${className}`, label, detailsKey, resultDetails(item));
|
||||
}
|
||||
|
||||
function toolView(
|
||||
|
|
@ -148,7 +150,7 @@ function toolView(
|
|||
label: string,
|
||||
detailsKey: string,
|
||||
details: ToolResultDetailSection[],
|
||||
summary = item.text,
|
||||
summary = fallbackSummary(item),
|
||||
): ToolResultView {
|
||||
return {
|
||||
className: `codex-panel__message codex-panel__message--tool ${className}`,
|
||||
|
|
@ -160,28 +162,181 @@ function toolView(
|
|||
};
|
||||
}
|
||||
|
||||
function resultDetailSection(section: MessageStreamDetailSection): ToolResultDetailSection[] {
|
||||
if (section.rows && section.rows.length > 0) return [{ kind: "meta", rows: section.rows }];
|
||||
return detailSection(section);
|
||||
}
|
||||
|
||||
function detailSection(section: MessageStreamDetailSection): ToolResultDetailSection[] {
|
||||
if (section.rows && section.rows.length > 0) return [{ kind: "meta", ...definedProp("title", section.title), rows: section.rows }];
|
||||
if (section.body) return [{ kind: "output", title: section.title ?? "Output", body: section.body }];
|
||||
return [];
|
||||
}
|
||||
|
||||
function outputSection(title: string, body: string | null | undefined): ToolResultDetailSection[] {
|
||||
return body ? [{ kind: "output", title, body }] : [];
|
||||
}
|
||||
|
||||
function fileChangeSummary(item: MessageStreamItem, changes: (MessageStreamFileChange & { displayPath: string })[]): string {
|
||||
if (item.kind !== "fileChange") return item.text;
|
||||
if (changes.length === 0) return item.text;
|
||||
if (changes.length > 1) return item.text;
|
||||
const relativePath = changes[0]?.displayPath;
|
||||
if (!relativePath || relativePath === "(unknown)") return item.text;
|
||||
const suffixMatch = /\s\(([^)]+)\)$/.exec(item.text);
|
||||
const suffix = suffixMatch?.[1];
|
||||
return suffix ? `${relativePath} (${suffix})` : relativePath;
|
||||
function goalDetails(item: GoalMessageStreamItem): ToolResultDetailSection[] {
|
||||
return [
|
||||
{
|
||||
kind: "meta",
|
||||
rows: [{ key: "action", value: item.action }],
|
||||
},
|
||||
...outputSection("Objective", item.objective),
|
||||
];
|
||||
}
|
||||
|
||||
function resultDetails(item: ApprovalResultMessageStreamItem | ReviewResultMessageStreamItem): ToolResultDetailSection[] {
|
||||
if (item.kind === "approvalResult") {
|
||||
return [
|
||||
{
|
||||
kind: "meta",
|
||||
rows: [
|
||||
{ key: "status", value: item.approval.status },
|
||||
{ key: "scope", value: item.approval.scope },
|
||||
{ key: "request", value: item.approval.request },
|
||||
...item.approval.auditFacts,
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
return item.review?.auditFacts && item.review.auditFacts.length > 0 ? [{ kind: "meta", rows: item.review.auditFacts }] : [];
|
||||
}
|
||||
|
||||
function genericToolDetails(item: ToolCallMessageStreamItem | HookMessageStreamItem): ToolResultDetailSection[] {
|
||||
if (item.kind === "hook") return hookRunDetails(item);
|
||||
return [...toolCallDetails(item), ...webSearchDetails(item), ...imageGenerationDetails(item)];
|
||||
}
|
||||
|
||||
function toolCallDetails(item: ToolCallMessageStreamItem): ToolResultDetailSection[] {
|
||||
const details = item.toolCall;
|
||||
if (!details) return [];
|
||||
return [
|
||||
...jsonOutputSection("Arguments JSON", details.arguments),
|
||||
...jsonOutputSection("Result JSON", details.result),
|
||||
...jsonOutputSection("Error JSON", details.error),
|
||||
];
|
||||
}
|
||||
|
||||
function webSearchDetails(item: ToolCallMessageStreamItem): ToolResultDetailSection[] {
|
||||
const details = item.webSearch;
|
||||
if (!details) return [];
|
||||
const rows = [
|
||||
...metaRow("action", details.action),
|
||||
...metaRow("query", details.query),
|
||||
...metaRow("pattern", details.pattern),
|
||||
...metaRow("url", details.url),
|
||||
];
|
||||
return rows.length > 0 ? [{ kind: "meta", title: "web search", rows }] : [];
|
||||
}
|
||||
|
||||
function imageGenerationDetails(item: ToolCallMessageStreamItem): ToolResultDetailSection[] {
|
||||
const details = item.imageGeneration;
|
||||
if (!details) return [];
|
||||
return [
|
||||
...outputSection("Saved path", details.savedPath),
|
||||
...outputSection("Revised prompt", details.revisedPrompt),
|
||||
...outputSection("Result", details.result),
|
||||
];
|
||||
}
|
||||
|
||||
function hookRunDetails(item: HookMessageStreamItem): ToolResultDetailSection[] {
|
||||
const details = item.hookRun;
|
||||
if (!details) return [];
|
||||
const rows = [
|
||||
...metaRow("status", item.status),
|
||||
{ key: "event", value: details.eventName },
|
||||
...metaRow("message", details.statusMessage),
|
||||
...metaRow("duration", details.durationMs),
|
||||
];
|
||||
const entries = details.entries.map((entry) => `${entry.kind}: ${entry.text}`).join("\n");
|
||||
return [{ kind: "meta", rows }, ...outputSection("Hook output", entries)];
|
||||
}
|
||||
|
||||
function jsonOutputSection(title: string, value: unknown): ToolResultDetailSection[] {
|
||||
return value === null || value === undefined ? [] : outputSection(title, jsonPreview(value));
|
||||
}
|
||||
|
||||
function metaRow(key: string, value: string | null | undefined): { key: string; value: string }[] {
|
||||
return value ? [{ key, value }] : [];
|
||||
}
|
||||
|
||||
function commandActionLabel(action: CommandMessageStreamItem["commandAction"]): string {
|
||||
if (action === "read") return "read";
|
||||
if (action === "search") return "search";
|
||||
if (action === "listFiles") return "list files";
|
||||
return "command";
|
||||
}
|
||||
|
||||
function commandSummary(item: CommandMessageStreamItem): string {
|
||||
return compactSummary(null, commandTargetSummary(item.commandTarget, item.cwd), commandQualifier(item));
|
||||
}
|
||||
|
||||
function commandTargetSummary(target: CommandMessageStreamTarget, cwd: string): string {
|
||||
if (target.kind === "read") return target.path ? pathRelativeToRoot(target.path, cwd) : target.name;
|
||||
if (target.kind === "search") {
|
||||
const query = target.query ? quoteInline(target.query) : null;
|
||||
const path = target.path ? pathRelativeToRoot(target.path, cwd) : null;
|
||||
if (query && path) return `${query} in ${path}`;
|
||||
if (query) return query;
|
||||
if (path) return path;
|
||||
return "search";
|
||||
}
|
||||
if (target.kind === "listFiles") return target.path ? pathRelativeToRoot(target.path, cwd) : "workspace";
|
||||
return target.commandLine;
|
||||
}
|
||||
|
||||
function commandQualifier(item: CommandMessageStreamItem): string | null {
|
||||
if (typeof item.exitCode === "number" && item.exitCode !== 0) return `exit ${String(item.exitCode)}`;
|
||||
return statusQualifier(item.status, failedStatusLabel(item.status));
|
||||
}
|
||||
|
||||
function genericToolSummary(item: ToolCallMessageStreamItem | HookMessageStreamItem, workspaceRoot?: string | null): string {
|
||||
const target = primaryTargetSummary(item.primaryTarget, workspaceRoot);
|
||||
if (!target) return item.text ?? "details";
|
||||
return compactSummary(toolOperationLabel(item.operation), target, statusQualifier(item.status, item.failureReason));
|
||||
}
|
||||
|
||||
function fileChangeSummary(item: MessageStreamItem, changes: (MessageStreamFileChange & { displayPath: string })[]): string {
|
||||
if (item.kind !== "fileChange") return "text" in item && typeof item.text === "string" ? item.text : "details";
|
||||
const target = fileChangeTargetSummary(changes);
|
||||
return compactSummary(null, target, statusQualifier(item.status, failedStatusLabel(item.status)));
|
||||
}
|
||||
|
||||
function fallbackSummary(item: ToolResultMessageStreamItem): string {
|
||||
return "text" in item && typeof item.text === "string" ? item.text : "details";
|
||||
}
|
||||
|
||||
function fileChangeTargetSummary(changes: (MessageStreamFileChange & { displayPath: string })[]): string {
|
||||
if (changes.length === 0) return "no files";
|
||||
if (changes.length === 1) return changes[0]?.displayPath ?? "1 file";
|
||||
return `${String(changes.length)} files`;
|
||||
}
|
||||
|
||||
function primaryTargetSummary(target: MessageStreamPrimaryTarget | undefined, workspaceRoot?: string | null): string | null {
|
||||
if (!target) return null;
|
||||
if (target.kind === "path") return pathRelativeToRoot(target.path, workspaceRoot);
|
||||
return target.value;
|
||||
}
|
||||
|
||||
function compactSummary(label: string | null, target?: string | null, qualifier?: string | null): string {
|
||||
const targetText = target?.trim();
|
||||
const base = label ? (targetText ? `${label}: ${targetText}` : label) : (targetText ?? "details");
|
||||
return truncate(qualifier ? `${base} (${qualifier})` : base, 140);
|
||||
}
|
||||
|
||||
function statusQualifier(status: unknown, failure?: string | null): string | null {
|
||||
if (status === "declined") return "declined";
|
||||
if (status === "failed") return failure && failure.length > 0 ? failure : "failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
function failedStatusLabel(status: unknown): string | null {
|
||||
if (status === "failed") return "failed";
|
||||
if (status === "declined") return "declined";
|
||||
return null;
|
||||
}
|
||||
|
||||
function toolOperationLabel(operation: string | null | undefined): string | null {
|
||||
if (!operation) return null;
|
||||
if (operation === "openPage") return "open page";
|
||||
if (operation === "findInPage") return "find in page";
|
||||
if (operation === "search") return "search";
|
||||
if (operation === "webSearch") return "web search";
|
||||
if (operation === "other") return "other";
|
||||
return operation;
|
||||
}
|
||||
|
||||
function quoteInline(value: string): string {
|
||||
return value.includes(" ") ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ describe("slash commands", () => {
|
|||
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith(
|
||||
"Thread goal",
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ rows: expect.arrayContaining([expect.objectContaining({ key: "objective", value: "Finish" })]) }),
|
||||
expect.objectContaining({ auditFacts: expect.arrayContaining([expect.objectContaining({ key: "objective", value: "Finish" })]) }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
|
@ -535,7 +535,7 @@ describe("slash commands", () => {
|
|||
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
|
||||
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Thread status", [
|
||||
{
|
||||
rows: [
|
||||
auditFacts: [
|
||||
{ key: "Thread", value: "thread-1" },
|
||||
{ key: "message", value: "Usage limits" },
|
||||
{ key: "5h", value: "42%" },
|
||||
|
|
@ -665,7 +665,7 @@ describe("slash commands", () => {
|
|||
await executeSlashCommand("mcp", "", ctx);
|
||||
|
||||
expect(ctx.mcpStatusLines).toHaveBeenCalledOnce();
|
||||
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("MCP servers", [{ rows: [] }]);
|
||||
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("MCP servers", [{ auditFacts: [] }]);
|
||||
});
|
||||
|
||||
it("rejects /mcp arguments", async () => {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ describe("goal message stream items", () => {
|
|||
expect(item).toMatchObject({
|
||||
kind: "goal",
|
||||
role: "tool",
|
||||
action: "set",
|
||||
objective,
|
||||
details: [{ rows: [{ key: "action", value: "set" }] }, { title: "Objective", body: objective }],
|
||||
});
|
||||
expect(item?.text.startsWith("set: Ship the feature")).toBe(true);
|
||||
expect(item?.text.length).toBeLessThan(objective.length);
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ function commandItem(id: string, text: string, turnId: string): MessageStreamIte
|
|||
id,
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text,
|
||||
turnId,
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: text },
|
||||
command: text,
|
||||
cwd: "/vault",
|
||||
status: "completed",
|
||||
|
|
@ -56,7 +57,6 @@ function fileChangeItem(id: string, turnId: string, path = "src/main.ts"): Messa
|
|||
id,
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: "File change completed",
|
||||
turnId,
|
||||
status: "completed",
|
||||
executionState: "completed",
|
||||
|
|
@ -87,7 +87,11 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
{ id: "old", items: [userMessage], itemsView: "full", status: "completed", startedAt: 1, completedAt: 2, durationMs: 1, error: null },
|
||||
];
|
||||
|
||||
expect(messageStreamItemsFromTurns(turns).map((item) => item.text)).toEqual(["hello", "world"]);
|
||||
expect(
|
||||
messageStreamItemsFromTurns(turns)
|
||||
.filter((item) => item.kind === "message")
|
||||
.map((item) => item.text),
|
||||
).toEqual(["hello", "world"]);
|
||||
expect(messageStreamItemFromTurnItem(userMessage)).toMatchObject({ role: "user", copyText: "hello" });
|
||||
expect(messageStreamItemFromTurnItem(assistantMessage)).toMatchObject({ role: "assistant", copyText: "world" });
|
||||
});
|
||||
|
|
@ -186,12 +190,12 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
it("preserves reasoning text", () => {
|
||||
const item: TurnItem = { type: "reasoning", id: "r1", summary: ["summary"], content: ["detail"] };
|
||||
expect(messageStreamItemFromTurnItem(item)?.text).toBe("summary\n\ndetail");
|
||||
expect(messageStreamItemFromTurnItem(item)).toMatchObject({ kind: "reasoning", text: "summary\n\ndetail" });
|
||||
});
|
||||
|
||||
it("keeps empty reasoning text empty so completed placeholders can be hidden", () => {
|
||||
const item: TurnItem = { type: "reasoning", id: "r1", summary: [], content: [] };
|
||||
expect(messageStreamItemFromTurnItem(item)?.text).toBe("");
|
||||
expect(messageStreamItemFromTurnItem(item)).toMatchObject({ kind: "reasoning", text: "" });
|
||||
});
|
||||
|
||||
it("renders completed proposed plans as copyable assistant markdown", () => {
|
||||
|
|
@ -255,7 +259,6 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
id: "plan-progress-t1",
|
||||
kind: "taskProgress",
|
||||
role: "tool",
|
||||
text: "Working plan\n[x] Inspect code\n[>] Patch UI\n[ ] Run tests",
|
||||
explanation: "Working plan",
|
||||
steps: [
|
||||
{ step: "Inspect code", status: "completed" },
|
||||
|
|
@ -334,7 +337,7 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
text: "npm run check (exit 1)",
|
||||
commandTarget: { kind: "command", commandLine: "npm run check" },
|
||||
output: "stderr with many details",
|
||||
executionState: "failed",
|
||||
});
|
||||
|
|
@ -357,8 +360,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "read",
|
||||
text: "src/main.ts",
|
||||
commandAction: "read",
|
||||
commandTarget: { kind: "read", path: "/vault/src/main.ts", name: "main.ts" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -380,8 +383,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "read",
|
||||
text: "src/main.ts",
|
||||
commandAction: "read",
|
||||
commandTarget: { kind: "read", path: "/vault/src/main.ts", name: "main.ts" },
|
||||
command: "nl -ba src/main.ts | sed -n '1,20p'",
|
||||
cwd: "/vault",
|
||||
executionState: "completed",
|
||||
|
|
@ -405,8 +408,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "read",
|
||||
text: "/vault/src/main.ts",
|
||||
commandAction: "read",
|
||||
commandTarget: { kind: "read", path: "/vault/src/main.ts", name: "main.ts" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -428,8 +431,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "command",
|
||||
text: "npm run check",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "npm run check" },
|
||||
command: "/bin/zsh -lc 'npm run check'",
|
||||
cwd: "/vault",
|
||||
executionState: "completed",
|
||||
|
|
@ -453,8 +456,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "search",
|
||||
text: '"command target" in src/display',
|
||||
commandAction: "search",
|
||||
commandTarget: { kind: "search", query: "command target", path: "src/display" },
|
||||
command: "rg 'command target' src/display",
|
||||
cwd: "/vault",
|
||||
executionState: "completed",
|
||||
|
|
@ -478,8 +481,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "search",
|
||||
text: "target in src/display",
|
||||
commandAction: "search",
|
||||
commandTarget: { kind: "search", query: "target", path: "/vault/src/display" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -501,8 +504,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "search",
|
||||
text: "target in src/display",
|
||||
commandAction: "search",
|
||||
commandTarget: { kind: "search", query: "target", path: "C:\\Vault\\src\\display" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -524,8 +527,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "list files",
|
||||
text: "src/display",
|
||||
commandAction: "listFiles",
|
||||
commandTarget: { kind: "listFiles", path: "src/display" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -550,8 +553,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "search",
|
||||
text: "target in src",
|
||||
commandAction: "search",
|
||||
commandTarget: { kind: "search", query: "target", path: "src" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -573,8 +576,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
actionLabel: "list files",
|
||||
text: "workspace",
|
||||
commandAction: "listFiles",
|
||||
commandTarget: { kind: "listFiles" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -596,7 +599,7 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "command",
|
||||
text: "rg error src",
|
||||
commandTarget: { kind: "command", commandLine: "rg error src" },
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -635,16 +638,16 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
};
|
||||
|
||||
expect(messageStreamItemFromTurnItem(command, "t1")).toMatchObject({
|
||||
text: "npm run check",
|
||||
commandTarget: { kind: "command", commandLine: "npm run check" },
|
||||
executionState: "running",
|
||||
});
|
||||
expect(messageStreamItemFromTurnItem(fileChange, "t1")).toMatchObject({
|
||||
text: "src/main.ts",
|
||||
changes: [{ path: "src/main.ts" }],
|
||||
executionState: "running",
|
||||
});
|
||||
expect(messageStreamItemFromTurnItem(tool, "t1")).toMatchObject({
|
||||
text: "123",
|
||||
toolLabel: "github.pull_request_read",
|
||||
primaryTarget: { kind: "value", value: "123" },
|
||||
toolName: "github.pull_request_read",
|
||||
executionState: "running",
|
||||
});
|
||||
});
|
||||
|
|
@ -665,12 +668,13 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "123 (Not found)",
|
||||
toolLabel: "github.pull_request_read",
|
||||
details: [
|
||||
{ title: "Arguments JSON", body: expect.stringContaining('"id": 123') },
|
||||
{ title: "Error JSON", body: expect.stringContaining("Not found") },
|
||||
],
|
||||
primaryTarget: { kind: "value", value: "123" },
|
||||
failureReason: "Not found",
|
||||
toolName: "github.pull_request_read",
|
||||
toolCall: {
|
||||
arguments: expect.objectContaining({ id: 123 }),
|
||||
error: { message: "Not found" },
|
||||
},
|
||||
executionState: "failed",
|
||||
});
|
||||
});
|
||||
|
|
@ -690,12 +694,12 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "https://example.com",
|
||||
toolLabel: "web.open",
|
||||
details: [
|
||||
{ title: "Arguments JSON", body: expect.stringContaining("https://example.com") },
|
||||
{ title: "Result JSON", body: expect.stringContaining("ok") },
|
||||
],
|
||||
primaryTarget: { kind: "value", value: "https://example.com" },
|
||||
toolName: "web.open",
|
||||
toolCall: {
|
||||
arguments: { url: "https://example.com" },
|
||||
result: [{ type: "inputText", text: "ok" }],
|
||||
},
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -709,9 +713,8 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "/vault/project/assets/image.png",
|
||||
toolLabel: "imageView",
|
||||
summaryPath: true,
|
||||
toolName: "imageView",
|
||||
primaryTarget: { kind: "path", path: "/vault/project/assets/image.png" },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -727,15 +730,14 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "/vault/project/assets/generated.png",
|
||||
toolLabel: "imageGeneration",
|
||||
summaryPath: true,
|
||||
toolName: "imageGeneration",
|
||||
primaryTarget: { kind: "path", path: "/vault/project/assets/generated.png" },
|
||||
status: "completed",
|
||||
details: [
|
||||
{ title: "Saved path", body: "/vault/project/assets/generated.png" },
|
||||
{ title: "Revised prompt", body: "A precise UI mockup." },
|
||||
{ title: "Result", body: "image result" },
|
||||
],
|
||||
imageGeneration: {
|
||||
savedPath: "/vault/project/assets/generated.png",
|
||||
revisedPrompt: "A precise UI mockup.",
|
||||
result: "image result",
|
||||
},
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -751,13 +753,12 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "image result",
|
||||
toolLabel: "imageGeneration",
|
||||
summaryPath: false,
|
||||
details: [
|
||||
{ title: "Revised prompt", body: "A precise UI mockup." },
|
||||
{ title: "Result", body: "image result" },
|
||||
],
|
||||
toolName: "imageGeneration",
|
||||
primaryTarget: { kind: "value", value: "image result" },
|
||||
imageGeneration: {
|
||||
revisedPrompt: "A precise UI mockup.",
|
||||
result: "image result",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -776,8 +777,7 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "details",
|
||||
toolLabel: "multi_tool_use.parallel",
|
||||
toolName: "multi_tool_use.parallel",
|
||||
executionState: "completed",
|
||||
});
|
||||
});
|
||||
|
|
@ -792,17 +792,13 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "search: codex app-server; obsidian codex panel; fallback query",
|
||||
toolLabel: "web search",
|
||||
details: [
|
||||
{
|
||||
title: "web search",
|
||||
rows: [
|
||||
{ key: "action", value: "search" },
|
||||
{ key: "query", value: "codex app-server; obsidian codex panel; fallback query" },
|
||||
],
|
||||
},
|
||||
],
|
||||
toolName: "web search",
|
||||
operation: "search",
|
||||
primaryTarget: { kind: "value", value: "codex app-server; obsidian codex panel; fallback query" },
|
||||
webSearch: {
|
||||
action: "search",
|
||||
query: "codex app-server; obsidian codex panel; fallback query",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -812,14 +808,14 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(entered, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "Entered review mode",
|
||||
toolLabel: "enteredReviewMode",
|
||||
toolName: "enteredReviewMode",
|
||||
primaryTarget: { kind: "value", value: "Entered review mode" },
|
||||
output: "Review started",
|
||||
});
|
||||
expect(messageStreamItemFromTurnItem(exited, "t1")).toMatchObject({
|
||||
kind: "tool",
|
||||
text: "Exited review mode",
|
||||
toolLabel: "exitedReviewMode",
|
||||
toolName: "exitedReviewMode",
|
||||
primaryTarget: { kind: "value", value: "Exited review mode" },
|
||||
output: "Review finished",
|
||||
});
|
||||
});
|
||||
|
|
@ -829,7 +825,6 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
|
||||
expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({
|
||||
kind: "contextCompaction",
|
||||
text: "Context compaction",
|
||||
turnId: "t1",
|
||||
sourceItemId: "compact-1",
|
||||
});
|
||||
|
|
@ -845,17 +840,14 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
kind: "reviewResult",
|
||||
text: "Auto-review approved: Auto-review returned a low-risk allow decision.",
|
||||
executionState: "completed",
|
||||
details: [
|
||||
{
|
||||
title: "Review",
|
||||
rows: [
|
||||
{ key: "status", value: "approved" },
|
||||
{ key: "risk", value: "low" },
|
||||
{ key: "authorization", value: "unknown" },
|
||||
{ key: "message", value: "Auto-review returned a low-risk allow decision." },
|
||||
],
|
||||
},
|
||||
],
|
||||
review: {
|
||||
auditFacts: [
|
||||
{ key: "status", value: "approved" },
|
||||
{ key: "risk", value: "low" },
|
||||
{ key: "authorization", value: "unknown" },
|
||||
{ key: "message", value: "Auto-review returned a low-risk allow decision." },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -875,16 +867,13 @@ describe("turn item conversion preserves app-server semantics", () => {
|
|||
).toMatchObject({
|
||||
kind: "reviewResult",
|
||||
text: "Auto-review approved: apply patch (2 files)",
|
||||
details: [
|
||||
{
|
||||
title: "Review",
|
||||
rows: expect.arrayContaining([
|
||||
{ key: "action", value: "apply patch" },
|
||||
{ key: "cwd", value: "/vault" },
|
||||
{ key: "files", value: "src/display/thread-items.ts\ntests/display-model.test.ts" },
|
||||
]),
|
||||
},
|
||||
],
|
||||
review: {
|
||||
auditFacts: expect.arrayContaining([
|
||||
{ key: "action", value: "apply patch" },
|
||||
{ key: "cwd", value: "/vault" },
|
||||
{ key: "files", value: "src/display/thread-items.ts\ntests/display-model.test.ts" },
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -933,10 +922,10 @@ describe("streaming updates target item identity without mutating history", () =
|
|||
];
|
||||
|
||||
const updated = appendAssistantDelta(items, "a1", "t1", " world");
|
||||
expect(expectPresent(updated[0]).text).toBe("hello world");
|
||||
expect(expectPresent(updated[0])).toMatchObject({ text: "hello world" });
|
||||
expect(expectPresent(updated[0])).toMatchObject({ copyText: "hello world" });
|
||||
expect(updated).toHaveLength(2);
|
||||
expect(expectPresent(items[0]).text).toBe("hello");
|
||||
expect(expectPresent(items[0])).toMatchObject({ text: "hello" });
|
||||
expect(updated).not.toBe(items);
|
||||
});
|
||||
|
||||
|
|
@ -947,7 +936,8 @@ describe("streaming updates target item identity without mutating history", () =
|
|||
sourceItemId: "cmd1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "Command running",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "Command running" },
|
||||
command: "npm test",
|
||||
cwd: "/vault",
|
||||
status: "running",
|
||||
|
|
@ -962,7 +952,7 @@ describe("streaming updates target item identity without mutating history", () =
|
|||
expect(withOutput[0]).toMatchObject({ output: "onetwo" });
|
||||
expect(withToolOutput[0]).toMatchObject({ text: "plan: ", output: "progress" });
|
||||
expect(tool.text).toBe("plan: ");
|
||||
expect(command.output).toBe("one");
|
||||
expect(command).toMatchObject({ output: "one" });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -996,7 +986,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
turnId: "t1",
|
||||
status: "completed",
|
||||
},
|
||||
|
|
@ -1149,7 +1139,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
|
|||
summary: "Work details: steer, 2 commands",
|
||||
items: [
|
||||
{ id: "c1" },
|
||||
{ id: "steer-activity-u2", text: "also check tests", activityKind: "userSteered", toolLabel: "steer" },
|
||||
{ id: "steer-activity-u2", text: "also check tests", activityKind: "userSteered", toolName: "steer" },
|
||||
{ id: "c2" },
|
||||
],
|
||||
});
|
||||
|
|
@ -1561,7 +1551,8 @@ describe("execution state uses typed status adapters before rendered text", () =
|
|||
id: "c1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "Command",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "Command" },
|
||||
command: "npm test",
|
||||
cwd: "/vault",
|
||||
status: "done_with_errors",
|
||||
|
|
@ -1575,7 +1566,8 @@ describe("execution state uses typed status adapters before rendered text", () =
|
|||
sourceItemId: "c1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "Command running",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "Command running" },
|
||||
command: "npm test",
|
||||
cwd: "/vault",
|
||||
status: "running",
|
||||
|
|
@ -1586,7 +1578,8 @@ describe("execution state uses typed status adapters before rendered text", () =
|
|||
sourceItemId: "c1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "npm test",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "npm test" },
|
||||
command: "npm test",
|
||||
cwd: "/vault",
|
||||
status: "completed",
|
||||
|
|
|
|||
|
|
@ -34,12 +34,11 @@ describe("timeline item semantics", () => {
|
|||
id: "patch",
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: "File change completed",
|
||||
status: "completed",
|
||||
changes: [{ kind: "update", path: "src/main.ts", diff: "@@" }],
|
||||
executionState: "completed",
|
||||
},
|
||||
{ id: "tool", kind: "tool", role: "tool", text: "tool", details: [{ rows: [{ key: "k", value: "v" }] }] },
|
||||
{ id: "tool", kind: "tool", role: "tool", text: "tool", toolCall: { arguments: { k: "v" } } },
|
||||
{ id: "hook", kind: "hook", role: "tool", text: "hook" },
|
||||
{ id: "reasoning", kind: "reasoning", role: "tool", text: "thinking" },
|
||||
]);
|
||||
|
|
@ -57,11 +56,17 @@ describe("timeline item semantics", () => {
|
|||
|
||||
it("classifies thread and interaction events by meaning before renderer shape", () => {
|
||||
const timeline = timelineItemsFromMessageStreamItems([
|
||||
{ id: "goal", kind: "goal", role: "tool", text: "set: Ship it" },
|
||||
{ id: "approval", kind: "approvalResult", role: "tool", text: "Approved" },
|
||||
{ id: "input", kind: "userInputResult", role: "tool", text: "Answered" },
|
||||
{ id: "goal", kind: "goal", role: "tool", text: "set: Ship it", action: "set" },
|
||||
{
|
||||
id: "approval",
|
||||
kind: "approvalResult",
|
||||
role: "tool",
|
||||
text: "Approved",
|
||||
approval: { status: "allowed", scope: "turn", request: "Approval", auditFacts: [] },
|
||||
},
|
||||
{ id: "input", kind: "userInputResult", role: "tool", text: "Answered", questions: [] },
|
||||
{ id: "review", kind: "reviewResult", role: "tool", text: "Auto-review approved" },
|
||||
{ id: "compact", kind: "contextCompaction", role: "tool", text: "Context compaction" },
|
||||
{ id: "compact", kind: "contextCompaction", role: "tool" },
|
||||
{ id: "system", kind: "system", role: "system", text: "Disconnected" },
|
||||
]);
|
||||
|
||||
|
|
@ -105,7 +110,8 @@ function commandItem(id: string): MessageStreamItem {
|
|||
id,
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "npm test",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "npm test" },
|
||||
command: "npm test",
|
||||
cwd: "/vault",
|
||||
status: "completed",
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ describe("ChatInboundController", () => {
|
|||
{
|
||||
id: "plan-progress-turn-active",
|
||||
kind: "taskProgress",
|
||||
text: "Plan\n[>] Inspect code",
|
||||
explanation: "Plan",
|
||||
steps: [{ step: "Inspect code", status: "inProgress" }],
|
||||
status: "inProgress",
|
||||
},
|
||||
|
|
@ -259,21 +259,17 @@ describe("ChatInboundController", () => {
|
|||
{
|
||||
id: "hook-hook-1-1",
|
||||
kind: "hook",
|
||||
text: "postToolUse: Formatted 1 file.",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
operation: "postToolUse",
|
||||
primaryTarget: { kind: "value", value: "Formatted 1 file." },
|
||||
status: "completed",
|
||||
output: "",
|
||||
details: [
|
||||
{
|
||||
rows: [
|
||||
{ key: "status", value: "completed" },
|
||||
{ key: "event", value: "postToolUse" },
|
||||
{ key: "message", value: "Formatted 1 file." },
|
||||
{ key: "duration", value: "1ms" },
|
||||
],
|
||||
},
|
||||
{ title: "Hook output", body: "feedback: ok" },
|
||||
],
|
||||
hookRun: {
|
||||
eventName: "postToolUse",
|
||||
statusMessage: "Formatted 1 file.",
|
||||
durationMs: "1ms",
|
||||
entries: [{ kind: "feedback", text: "ok" }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -310,14 +306,10 @@ describe("ChatInboundController", () => {
|
|||
|
||||
expect(chatStateMessageStreamItems(state)[0]).toMatchObject({
|
||||
kind: "hook",
|
||||
details: [
|
||||
{
|
||||
rows: [
|
||||
{ key: "status", value: "completed" },
|
||||
{ key: "event", value: "postToolUse" },
|
||||
],
|
||||
},
|
||||
],
|
||||
hookRun: {
|
||||
eventName: "postToolUse",
|
||||
entries: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -436,7 +428,7 @@ describe("ChatInboundController", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
status: "completed",
|
||||
},
|
||||
]);
|
||||
|
|
@ -472,7 +464,7 @@ describe("ChatInboundController", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Stale hook",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
|
|
@ -480,7 +472,7 @@ describe("ChatInboundController", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
status: "completed",
|
||||
},
|
||||
{ id: "local-user-1", kind: "message", messageKind: "user", role: "user", text: "hello" },
|
||||
|
|
@ -613,7 +605,7 @@ describe("ChatInboundController", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
status: "completed",
|
||||
},
|
||||
]);
|
||||
|
|
@ -726,7 +718,7 @@ describe("ChatInboundController", () => {
|
|||
role: "tool",
|
||||
text: "Input submitted for 1 question.",
|
||||
turnId: "turn-active",
|
||||
details: [{ title: "Question: Scope", rows: expect.arrayContaining([{ key: "Answer", value: "Narrow" }]) }],
|
||||
questions: [expect.objectContaining({ id: "scope", header: "Scope", answer: "Narrow" })],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -796,17 +788,12 @@ describe("ChatInboundController", () => {
|
|||
text: "Allowed for this session: Need access",
|
||||
turnId: "turn",
|
||||
executionState: "completed",
|
||||
details: [
|
||||
{
|
||||
title: "Approval",
|
||||
rows: expect.arrayContaining([
|
||||
{ key: "status", value: "allowed for session" },
|
||||
{ key: "scope", value: "session" },
|
||||
{ key: "request", value: "Permission approval" },
|
||||
{ key: "cwd", value: "/tmp/project" },
|
||||
]),
|
||||
},
|
||||
],
|
||||
approval: {
|
||||
status: "allowed for session",
|
||||
scope: "session",
|
||||
request: "Permission approval",
|
||||
auditFacts: expect.arrayContaining([{ key: "cwd", value: "/tmp/project" }]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -854,10 +841,10 @@ describe("ChatInboundController", () => {
|
|||
-32601,
|
||||
"Rejected unknown app-server request: appServer/newFutureRequest",
|
||||
);
|
||||
expect(chatStateMessageStreamItems(state).map((item) => item.text)).toEqual(unsupportedMessages);
|
||||
expect(chatStateMessageStreamItems(state).map((item) => ("text" in item ? item.text : ""))).toEqual(unsupportedMessages);
|
||||
expect(
|
||||
chatStateMessageStreamItems(state)
|
||||
.map((item) => item.text)
|
||||
.map((item) => ("text" in item ? item.text : ""))
|
||||
.join("\n"),
|
||||
).not.toContain("do-not-render");
|
||||
});
|
||||
|
|
@ -1676,10 +1663,7 @@ describe("ChatInboundController", () => {
|
|||
executionState: "completed",
|
||||
});
|
||||
const reviewItem = expectPresent(chatStateMessageStreamItems(state)[0]);
|
||||
expect("details" in reviewItem ? reviewItem.details[0] : null).toMatchObject({
|
||||
title: "Review",
|
||||
rows: expect.arrayContaining([{ key: "status", value: "approved" }]),
|
||||
});
|
||||
expect(reviewItem).toMatchObject({ review: { auditFacts: expect.arrayContaining([{ key: "status", value: "approved" }]) } });
|
||||
});
|
||||
|
||||
it("replaces guardian auto-review warnings when structured auto-review notifications arrive", () => {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,26 @@ describe("message stream selectors", () => {
|
|||
expect(messageStreamRollbackCandidate(state)).toEqual({ turnId: "turn-3", itemId: "u3", text: "third" });
|
||||
});
|
||||
|
||||
it("restores the raw user message text instead of rendered display text", () => {
|
||||
const state = initialChatMessageStreamState([
|
||||
{
|
||||
id: "u1",
|
||||
kind: "message",
|
||||
messageKind: "user",
|
||||
role: "user",
|
||||
text: "Use `$obsidian-codex-panel-maintain`.",
|
||||
copyText: "Use $obsidian-codex-panel-maintain.",
|
||||
turnId: "turn-1",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(messageStreamRollbackCandidate(state)).toEqual({
|
||||
turnId: "turn-1",
|
||||
itemId: "u1",
|
||||
text: "Use $obsidian-codex-panel-maintain.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when rollback has no user message candidate", () => {
|
||||
expect(messageStreamRollbackCandidate(initialChatMessageStreamState([]))).toBeNull();
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ describe("createGoalActions", () => {
|
|||
kind: "goal",
|
||||
role: "tool",
|
||||
text: "set: Finish",
|
||||
action: "set",
|
||||
objective: "Finish",
|
||||
details: [{ rows: [{ key: "action", value: "set" }] }, { title: "Objective", body: "Finish" }],
|
||||
}),
|
||||
);
|
||||
expect(injectThreadItems).toHaveBeenCalledWith("thread", [
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ describe("chat thread title context", () => {
|
|||
kind: "goal",
|
||||
role: "tool",
|
||||
text: "Goal set.",
|
||||
action: "set",
|
||||
objective: "ゴールから始めたスレッドを命名したい",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe("message stream rendering and message actions", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
turnId: "t1",
|
||||
status: "completed",
|
||||
},
|
||||
|
|
@ -131,16 +131,13 @@ describe("message stream rendering and message actions", () => {
|
|||
text: "Auto-review approved: npm test",
|
||||
turnId: "turn",
|
||||
executionState: "completed",
|
||||
details: [
|
||||
{
|
||||
title: "Review",
|
||||
rows: [
|
||||
{ key: "status", value: "approved" },
|
||||
{ key: "action", value: "apply patch" },
|
||||
{ key: "files", value: "src/ui/tool-result-view.ts\nsrc/ui/message-stream.ts" },
|
||||
],
|
||||
},
|
||||
],
|
||||
review: {
|
||||
auditFacts: [
|
||||
{ key: "status", value: "approved" },
|
||||
{ key: "action", value: "apply patch" },
|
||||
{ key: "files", value: "src/ui/tool-result-view.ts\nsrc/ui/message-stream.ts" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
@ -177,9 +174,9 @@ describe("message stream rendering and message actions", () => {
|
|||
kind: "system",
|
||||
role: "system",
|
||||
text: "Available slash commands",
|
||||
details: [
|
||||
noticeSections: [
|
||||
{
|
||||
rows: [
|
||||
auditFacts: [
|
||||
{ key: "/help", value: "Show available Codex slash commands." },
|
||||
{ key: "/resume [thread]", value: "Resume a recent Codex thread." },
|
||||
],
|
||||
|
|
@ -217,8 +214,8 @@ describe("message stream rendering and message actions", () => {
|
|||
kind: "goal",
|
||||
role: "tool",
|
||||
text: "set: Ship the feature",
|
||||
action: "set",
|
||||
objective: "Ship the feature",
|
||||
details: [{ rows: [{ key: "action", value: "set" }] }, { title: "Objective", body: "Ship the feature" }],
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
@ -855,7 +852,7 @@ describe("message stream rendering and message actions", () => {
|
|||
role: "tool",
|
||||
text: "tool summary",
|
||||
turnId: "turn",
|
||||
toolLabel: "web search",
|
||||
toolName: "web search",
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
@ -1048,7 +1045,8 @@ describe("message stream rendering and message actions", () => {
|
|||
id: "cmd-1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "npm run check (exit 1)",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "npm run check" },
|
||||
turnId: "turn",
|
||||
command: "npm run check",
|
||||
cwd: "/vault",
|
||||
|
|
@ -1086,7 +1084,8 @@ describe("message stream rendering and message actions", () => {
|
|||
id: "cmd-1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
text: "npm run check",
|
||||
commandAction: "command",
|
||||
commandTarget: { kind: "command", commandLine: "npm run check" },
|
||||
turnId: "turn",
|
||||
command: "npm run check",
|
||||
cwd: "/vault",
|
||||
|
|
@ -1121,8 +1120,8 @@ describe("message stream rendering and message actions", () => {
|
|||
id: "cmd-1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
actionLabel: "read",
|
||||
text: "sed /vault/src/main.ts",
|
||||
commandAction: "read",
|
||||
commandTarget: { kind: "read", path: "/vault/src/main.ts", name: "main.ts" },
|
||||
turnId: "turn",
|
||||
command: "sed -n '1,20p' src/main.ts",
|
||||
cwd: "/vault",
|
||||
|
|
@ -1142,6 +1141,37 @@ describe("message stream rendering and message actions", () => {
|
|||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["read"]);
|
||||
});
|
||||
|
||||
it("derives command summaries from semantic command targets instead of item text", () => {
|
||||
const block = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: runningTurnLifecycle("turn"),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
items: [
|
||||
{
|
||||
id: "cmd-1",
|
||||
kind: "command",
|
||||
role: "tool",
|
||||
commandAction: "search",
|
||||
commandTarget: { kind: "search", query: "semantic target", path: "/vault/src" },
|
||||
turnId: "turn",
|
||||
command: "rg 'semantic target' /vault/src",
|
||||
cwd: "/vault",
|
||||
status: "completed",
|
||||
output: "results",
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
forkActionsItemId: null,
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
})[0];
|
||||
|
||||
const element = renderMessageBlockElement(block);
|
||||
|
||||
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe('"semantic target" in src');
|
||||
});
|
||||
|
||||
it("renders file diffs inside a single file change details block", () => {
|
||||
const block = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
|
|
@ -1154,7 +1184,6 @@ describe("message stream rendering and message actions", () => {
|
|||
id: "patch-1",
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: "/vault/project/src/main.ts",
|
||||
turnId: "turn",
|
||||
status: "completed",
|
||||
changes: [{ kind: "update", path: "/vault/project/src/main.ts", diff: "@@\n-old\n+new" }],
|
||||
|
|
@ -1179,6 +1208,34 @@ describe("message stream rendering and message actions", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("derives file change summaries from changes and status instead of item text", () => {
|
||||
const block = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: runningTurnLifecycle("turn"),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
workspaceRoot: "/vault/project",
|
||||
items: [
|
||||
{
|
||||
id: "patch-1",
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
turnId: "turn",
|
||||
status: "failed",
|
||||
changes: [{ kind: "update", path: "/vault/project/src/main.ts", diff: "@@\n-old\n+new" }],
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
forkActionsItemId: null,
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
})[0];
|
||||
|
||||
const element = renderMessageBlockElement(block);
|
||||
|
||||
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("src/main.ts (failed)");
|
||||
});
|
||||
|
||||
it("renders the edited files footer with an open diff action when aggregated turn diff exists", () => {
|
||||
const openTurnDiff = vi.fn();
|
||||
const blocks = messageStreamBlocks({
|
||||
|
|
@ -1192,7 +1249,6 @@ describe("message stream rendering and message actions", () => {
|
|||
id: "patch-1",
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: "File change completed",
|
||||
turnId: "turn",
|
||||
status: "completed",
|
||||
changes: [{ kind: "update", path: "/vault/project/src/main.ts", diff: "@@\n-old\n+new" }],
|
||||
|
|
@ -1314,7 +1370,6 @@ describe("message stream rendering and message actions", () => {
|
|||
id: "patch-1",
|
||||
kind: "fileChange",
|
||||
role: "tool",
|
||||
text: "File change completed",
|
||||
turnId: "turn",
|
||||
status: "completed",
|
||||
changes: [{ kind: "update", path: "src/main.ts", diff: "@@\n-old\n+new" }],
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ describe("pending request renderer decisions", () => {
|
|||
text: "Input submitted for 1 question.",
|
||||
turnId: "turn",
|
||||
executionState: "completed",
|
||||
details: [{ title: "Question: Scope", rows: [{ key: "Answer", value: "Narrow" }] }],
|
||||
questions: [{ id: "scope", header: "Scope", question: "How broad?", answer: "Narrow" }],
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
@ -404,15 +404,12 @@ describe("pending request renderer decisions", () => {
|
|||
text: "Allowed for this session: Need access",
|
||||
turnId: "turn",
|
||||
executionState: "completed",
|
||||
details: [
|
||||
{
|
||||
title: "Approval",
|
||||
rows: [
|
||||
{ key: "status", value: "allowed for session" },
|
||||
{ key: "scope", value: "session" },
|
||||
],
|
||||
},
|
||||
],
|
||||
approval: {
|
||||
status: "allowed for session",
|
||||
scope: "session",
|
||||
request: "Permission approval",
|
||||
auditFacts: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ describe("work log renderer decisions", () => {
|
|||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "123",
|
||||
toolLabel: "github.pull_request_read",
|
||||
toolName: "github.pull_request_read",
|
||||
turnId: "turn",
|
||||
status: "completed",
|
||||
details: [
|
||||
{ title: "Arguments JSON", body: '{\n "id": 123\n}' },
|
||||
{ title: "Result JSON", body: '{\n "ok": true\n}' },
|
||||
],
|
||||
toolCall: {
|
||||
arguments: { id: 123 },
|
||||
result: { ok: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
@ -109,8 +109,8 @@ describe("work log renderer decisions", () => {
|
|||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "/vault/project/assets/image.png",
|
||||
toolLabel: "imageView",
|
||||
summaryPath: true,
|
||||
toolName: "imageView",
|
||||
primaryTarget: { kind: "path", path: "/vault/project/assets/image.png" },
|
||||
turnId: "turn",
|
||||
},
|
||||
],
|
||||
|
|
@ -126,6 +126,36 @@ describe("work log renderer decisions", () => {
|
|||
expect(element.querySelector(".codex-panel__tool-summary")?.getAttribute("title")).toBeNull();
|
||||
});
|
||||
|
||||
it("derives generic tool summaries from primary targets instead of item text", () => {
|
||||
const block = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: runningTurnLifecycle("turn"),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
workspaceRoot: "/vault/project",
|
||||
items: [
|
||||
{
|
||||
id: "tool-1",
|
||||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "legacy text",
|
||||
toolName: "web search",
|
||||
operation: "search",
|
||||
primaryTarget: { kind: "value", value: "codex app-server" },
|
||||
turnId: "turn",
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
forkActionsItemId: null,
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
})[0];
|
||||
|
||||
const element = renderMessageBlockElement(block);
|
||||
|
||||
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("search: codex app-server");
|
||||
});
|
||||
|
||||
it("updates path summary tools when the workspace root changes", () => {
|
||||
const parent = document.createElement("div");
|
||||
const item = {
|
||||
|
|
@ -133,8 +163,8 @@ describe("work log renderer decisions", () => {
|
|||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "/vault/project/assets/image.png",
|
||||
toolLabel: "imageView",
|
||||
summaryPath: true,
|
||||
toolName: "imageView",
|
||||
primaryTarget: { kind: "path", path: "/vault/project/assets/image.png" },
|
||||
turnId: "turn",
|
||||
} as const;
|
||||
const baseContext = {
|
||||
|
|
@ -170,8 +200,8 @@ describe("work log renderer decisions", () => {
|
|||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "/tmp/image.png",
|
||||
toolLabel: "imageView",
|
||||
summaryPath: true,
|
||||
toolName: "imageView",
|
||||
primaryTarget: { kind: "path", path: "/tmp/image.png" },
|
||||
turnId: "turn",
|
||||
},
|
||||
],
|
||||
|
|
@ -199,7 +229,7 @@ describe("work log renderer decisions", () => {
|
|||
kind: "tool",
|
||||
role: "tool",
|
||||
text: "/vault/project",
|
||||
toolLabel: "example.tool",
|
||||
toolName: "example.tool",
|
||||
turnId: "turn",
|
||||
},
|
||||
],
|
||||
|
|
@ -226,20 +256,15 @@ describe("work log renderer decisions", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "postToolUse: Formatted 1 file.",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
turnId: "turn",
|
||||
status: "completed",
|
||||
details: [
|
||||
{
|
||||
rows: [
|
||||
{ key: "status", value: "completed" },
|
||||
{ key: "event", value: "postToolUse" },
|
||||
{ key: "message", value: "Formatted 1 file." },
|
||||
{ key: "duration", value: "1ms" },
|
||||
],
|
||||
},
|
||||
{ title: "Hook output", body: "feedback: ok" },
|
||||
],
|
||||
hookRun: {
|
||||
eventName: "postToolUse",
|
||||
statusMessage: "Formatted 1 file.",
|
||||
durationMs: "1ms",
|
||||
entries: [{ kind: "feedback", text: "ok" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
disclosures: emptyDisclosures(),
|
||||
|
|
@ -274,19 +299,14 @@ describe("work log renderer decisions", () => {
|
|||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "postToolUse: Formatted 1 file.",
|
||||
toolLabel: "hook",
|
||||
toolName: "hook",
|
||||
turnId: "turn",
|
||||
status: "completed",
|
||||
details: [
|
||||
{
|
||||
rows: [
|
||||
{ key: "status", value: "completed" },
|
||||
{ key: "event", value: "postToolUse" },
|
||||
{ key: "message", value: "Formatted 1 file." },
|
||||
],
|
||||
},
|
||||
{ title: "Hook output", body: "feedback: ok" },
|
||||
],
|
||||
hookRun: {
|
||||
eventName: "postToolUse",
|
||||
statusMessage: "Formatted 1 file.",
|
||||
entries: [{ kind: "feedback", text: "ok" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
|
|
@ -707,7 +727,6 @@ describe("work log renderer decisions", () => {
|
|||
id: "compact-1",
|
||||
kind: "contextCompaction",
|
||||
role: "tool",
|
||||
text: "Context compaction",
|
||||
turnId: "turn",
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue