mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Align internal thread stream vocabulary
This commit is contained in:
parent
d81044c17b
commit
36ed7fceb3
146 changed files with 3062 additions and 3076 deletions
|
|
@ -60,11 +60,11 @@
|
|||
},
|
||||
{
|
||||
"path": "./scripts/grit/import-boundaries/no-app-server-protocol-boundary-imports.grit",
|
||||
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/app-server/**", "!**/src/features/chat/app-server/mappers/message-stream/turn-items.ts"]
|
||||
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/app-server/**", "!**/src/features/chat/app-server/mappers/thread-stream/turn-items.ts"]
|
||||
},
|
||||
{
|
||||
"path": "./scripts/grit/import-boundaries/no-chat-turn-item-non-turn-protocol-imports.grit",
|
||||
"includes": ["**/src/features/chat/app-server/mappers/message-stream/turn-items.ts"]
|
||||
"includes": ["**/src/features/chat/app-server/mappers/thread-stream/turn-items.ts"]
|
||||
},
|
||||
{
|
||||
"path": "./scripts/grit/import-boundaries/no-generated-app-server-boundary-imports.grit",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ Do not hide complexity behind forwarding layers. Add an abstraction only when it
|
|||
|
||||
## UI Ownership
|
||||
|
||||
Runtime UI composition is Preact-owned. Preact components should render the panel shell, toolbar, message stream, composer, and request controls.
|
||||
Runtime UI composition is Preact-owned. Preact components should render the panel shell, toolbar, thread stream, composer, and request controls.
|
||||
|
||||
Obsidian and app-server boundaries stay outside Preact components. External lifecycles, app-server connections, editor/workspace APIs, and rendering bridges belong in boundary modules.
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ Selection rewrite is intentionally scoped to a focused edit-and-review workflow.
|
|||
|
||||
Server requests should become panel UI only when the user can naturally answer them in context. Unknown or unsupported requests should stay diagnostic instead of pretending to be normal conversation text.
|
||||
|
||||
Message stream display should separate primary conversation from diagnostic detail and progress/status. Preserve stable item identity across history, streaming, and rendering updates.
|
||||
Thread stream display should separate primary conversation from diagnostic detail and progress/status. Preserve stable item identity across history, streaming, and rendering updates.
|
||||
|
||||
Codex Panel UI should feel native inside Obsidian. Prefer Obsidian variables, standard classes, and side-panel patterns. Add custom visual treatment only when Codex-specific state would otherwise be hard to read.
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ Prefer functions and factory-created objects over classes. Use a class only when
|
|||
- Build before Obsidian validation. Obsidian loads ignored root assets, not the TypeScript or authored CSS sources directly.
|
||||
- Preserve last-known-good app-server state on refresh failure. Do not turn disconnected reads into authoritative empty thread lists, settings snapshots, hook inventories, or diagnostics.
|
||||
- Normalize optional and nullable app-server values before display. Users should not see raw `undefined`, `null`, protocol enum gaps, or fallback labels that imply Panel owns a Codex runtime setting.
|
||||
- Do not use DOM order as message history state. Message stream DOM is a presentation surface, and delayed Markdown rendering can change heights after initial render.
|
||||
- Do not use DOM order as thread history state. Thread stream DOM is a presentation surface, and delayed Markdown rendering can change heights after initial render.
|
||||
|
||||
## API Baselines
|
||||
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@ import {
|
|||
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
|
||||
import type { ThreadCatalogEvent } from "../../../threads/catalog/thread-catalog";
|
||||
import type { AppServerResourceEvent } from "../../application/connection/server-metadata-actions";
|
||||
import { activeTurnId } from "../../application/conversation/turn-state";
|
||||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import type { ChatAction, ChatState } from "../../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../../domain/message-stream/factories/system-items";
|
||||
import type { MessageStreamNoticeSection } from "../../domain/message-stream/items";
|
||||
import { activeTurnId } from "../../application/turns/turn-state";
|
||||
import {
|
||||
createApprovalResultItem,
|
||||
createMcpElicitationResultItem,
|
||||
createUserInputResultItem,
|
||||
} from "../../domain/pending-requests/result-items";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../../domain/thread-stream/factories/system-items";
|
||||
import type { ThreadStreamNoticeSection } from "../../domain/thread-stream/items";
|
||||
import { classifyAppServerLog } from "./app-server-logs";
|
||||
import { type ChatNotificationEffect, planChatNotification } from "./notification-plan";
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export interface ChatInboundHandler {
|
|||
cancelUserInput(requestId: PendingRequestId): void;
|
||||
resolveMcpElicitation(requestId: PendingRequestId, action: McpElicitationAction): void;
|
||||
addSystemMessage(text: string): void;
|
||||
addStructuredSystemMessage(text: string, details: MessageStreamNoticeSection[]): void;
|
||||
addStructuredSystemMessage(text: string, details: ThreadStreamNoticeSection[]): void;
|
||||
addDedupedSystemMessage(text: string): void;
|
||||
}
|
||||
|
||||
|
|
@ -213,18 +213,18 @@ function pendingUserInput(context: ChatInboundHandlerContext, requestId: Pending
|
|||
}
|
||||
|
||||
function addSystemMessage(context: ChatInboundHandlerContext, text: string): void {
|
||||
dispatch(context, { type: "message-stream/system-item-added", item: createSystemItem(localItemId(context, "system"), text) });
|
||||
dispatch(context, { type: "thread-stream/system-item-added", item: createSystemItem(localItemId(context, "system"), text) });
|
||||
}
|
||||
|
||||
function addStructuredSystemMessage(context: ChatInboundHandlerContext, text: string, details: MessageStreamNoticeSection[]): void {
|
||||
function addStructuredSystemMessage(context: ChatInboundHandlerContext, text: string, details: ThreadStreamNoticeSection[]): void {
|
||||
dispatch(context, {
|
||||
type: "message-stream/system-item-added",
|
||||
type: "thread-stream/system-item-added",
|
||||
item: createStructuredSystemItem(localItemId(context, "system"), text, details),
|
||||
});
|
||||
}
|
||||
|
||||
function addDedupedSystemMessage(context: ChatInboundHandlerContext, text: string): void {
|
||||
dispatch(context, { type: "message-stream/deduped-log-added", text, item: createSystemItem(localItemId(context, "system"), text) });
|
||||
dispatch(context, { type: "thread-stream/deduped-log-added", text, item: createSystemItem(localItemId(context, "system"), text) });
|
||||
}
|
||||
|
||||
function rejectServerRequest(context: ChatInboundHandlerContext, request: ServerRequest, message: string): void {
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import { threadTokenUsageFromRuntimeUsage } from "../../../../domain/runtime/met
|
|||
import { normalizeExplicitThreadName } from "../../../../domain/threads/model";
|
||||
import type { ThreadCatalogEvent } from "../../../threads/catalog/thread-catalog";
|
||||
import type { AppServerResourceEvent } from "../../application/connection/server-metadata-actions";
|
||||
import { type ConversationRuntimeOutcome, planConversationRuntimeEvents } from "../../application/conversation/runtime-event-plan";
|
||||
import { activeThreadSettingsAppliedAction } from "../../application/state/actions";
|
||||
import type { ChatAction, ChatState } from "../../application/state/root-reducer";
|
||||
import { goalChangeItem } from "../../domain/message-stream/factories/goal-items";
|
||||
import { planTurnRuntimeEvents, type TurnRuntimeOutcome } from "../../application/turns/runtime-event-plan";
|
||||
import { goalChangeItem } from "../../domain/thread-stream/factories/goal-items";
|
||||
import { type DiagnosticStatusNotification, routeServerNotification, type ThreadLifecycleNotification } from "./notification-routing";
|
||||
import { conversationRuntimeEventsFromNotification } from "./runtime-events";
|
||||
import { turnRuntimeEventsFromNotification } from "./runtime-events";
|
||||
|
||||
export type ChatNotificationEffect =
|
||||
| { type: "refresh-threads" }
|
||||
| { type: "maybe-name-thread"; threadId: string; turnId: string; completedSummary: ConversationRuntimeCompletedSummary }
|
||||
| { type: "maybe-name-thread"; threadId: string; turnId: string; completedSummary: TurnRuntimeCompletedSummary }
|
||||
| { type: "refresh-server-diagnostics"; forceResourceProbes?: boolean }
|
||||
| { type: "apply-app-server-resource-event"; event: AppServerResourceEvent }
|
||||
| { type: "apply-thread-catalog-event"; event: ThreadCatalogEvent };
|
||||
|
||||
type ConversationRuntimeCompletedSummary = Extract<ConversationRuntimeOutcome, { type: "run-completed" }>["completedSummary"];
|
||||
type TurnRuntimeCompletedSummary = Extract<TurnRuntimeOutcome, { type: "turn-completed" }>["completedSummary"];
|
||||
|
||||
export interface ChatNotificationPlan {
|
||||
actions: readonly ChatAction[];
|
||||
|
|
@ -57,28 +57,28 @@ export function planChatNotification(
|
|||
|
||||
function runtimeEventsPlan(
|
||||
state: ChatState,
|
||||
notification: Parameters<typeof conversationRuntimeEventsFromNotification>[0],
|
||||
notification: Parameters<typeof turnRuntimeEventsFromNotification>[0],
|
||||
localItemId: LocalItemIdProvider,
|
||||
): ChatNotificationPlan {
|
||||
const plan = planConversationRuntimeEvents(state, conversationRuntimeEventsFromNotification(notification, localItemId));
|
||||
return { actions: plan.actions, effects: plan.outcomes.flatMap(chatNotificationEffectsFromConversationRuntimeOutcome) };
|
||||
const plan = planTurnRuntimeEvents(state, turnRuntimeEventsFromNotification(notification, localItemId));
|
||||
return { actions: plan.actions, effects: plan.outcomes.flatMap(chatNotificationEffectsFromTurnRuntimeOutcome) };
|
||||
}
|
||||
|
||||
function chatNotificationEffectsFromConversationRuntimeOutcome(outcome: ConversationRuntimeOutcome): readonly ChatNotificationEffect[] {
|
||||
function chatNotificationEffectsFromTurnRuntimeOutcome(outcome: TurnRuntimeOutcome): readonly ChatNotificationEffect[] {
|
||||
switch (outcome.type) {
|
||||
case "run-started":
|
||||
case "turn-started":
|
||||
return [
|
||||
{
|
||||
type: "apply-thread-catalog-event",
|
||||
event: { type: "thread-touched", threadId: outcome.threadId, recencyAt: outcome.recencyAt },
|
||||
},
|
||||
];
|
||||
case "run-completed":
|
||||
case "turn-completed":
|
||||
return [
|
||||
{
|
||||
type: "maybe-name-thread",
|
||||
threadId: outcome.threadId,
|
||||
turnId: outcome.runId,
|
||||
turnId: outcome.turnId,
|
||||
completedSummary: outcome.completedSummary,
|
||||
},
|
||||
{ type: "refresh-threads" },
|
||||
|
|
@ -178,7 +178,7 @@ function threadGoalPlan(
|
|||
if (state.activeThread.id !== threadId) return EMPTY_PLAN;
|
||||
const actions: ChatAction[] = [{ type: "active-thread/goal-set", goal }];
|
||||
const item = goalChangeItem(localItemId("goal"), state.activeThread.goal, goal);
|
||||
if (item) actions.push({ type: "message-stream/item-upserted", item });
|
||||
if (item) actions.push({ type: "thread-stream/item-upserted", item });
|
||||
return { actions, effects: [] };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import type { ServerNotification } from "../../../../app-server/connection/rpc-messages";
|
||||
import { jsonPreview } from "../../../../domain/display/json-preview";
|
||||
import type { ConversationRuntimeEvent } from "../../application/conversation/runtime-events";
|
||||
import type { TurnRuntimeEvent } from "../../application/turns/runtime-events";
|
||||
import {
|
||||
STREAMED_COMMAND_RUNNING_TEXT,
|
||||
STREAMED_FILE_CHANGE_IN_PROGRESS_TEXT,
|
||||
STREAMED_MCP_PROGRESS_LABEL,
|
||||
} from "../../domain/message-stream/factories/streaming-items";
|
||||
import { createSystemItem } from "../../domain/message-stream/factories/system-items";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import {
|
||||
type AppServerFileChange,
|
||||
normalizeFileChanges,
|
||||
streamingFileChangeMessageStreamItem,
|
||||
} from "../mappers/message-stream/file-changes";
|
||||
import { hookRunMessageStreamItem } from "../mappers/message-stream/hook-run-items";
|
||||
import { createAutoReviewResultItem, createReviewResultItem } from "../mappers/message-stream/review-result-items";
|
||||
import { taskProgressMessageStreamItem } from "../mappers/message-stream/task-progress";
|
||||
} from "../../domain/thread-stream/factories/streaming-items";
|
||||
import { createSystemItem } from "../../domain/thread-stream/factories/system-items";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { type AppServerFileChange, normalizeFileChanges, streamingFileChangeThreadStreamItem } from "../mappers/thread-stream/file-changes";
|
||||
import { hookRunThreadStreamItem } from "../mappers/thread-stream/hook-run-items";
|
||||
import { createAutoReviewResultItem, createReviewResultItem } from "../mappers/thread-stream/review-result-items";
|
||||
import { taskProgressThreadStreamItem } from "../mappers/thread-stream/task-progress";
|
||||
import {
|
||||
type AppServerTurnItem,
|
||||
completedConversationSummaryFromAppServerTurn,
|
||||
messageStreamItemFromTurnItem,
|
||||
messageStreamItemsFromTurns,
|
||||
shouldSuppressLifecycleItem,
|
||||
} from "../mappers/message-stream/turn-items";
|
||||
threadStreamItemFromTurnItem,
|
||||
threadStreamItemsFromTurns,
|
||||
} from "../mappers/thread-stream/turn-items";
|
||||
import type { StreamUpdateNotification, TurnLifecycleNotification, UserVisibleNoticeNotification } from "./notification-routing";
|
||||
|
||||
const MESSAGE_CONTEXT_COMPACTED = "Context compacted.";
|
||||
|
|
@ -33,17 +29,17 @@ type RuntimeEventSource =
|
|||
| Extract<ServerNotification, { method: "serverRequest/resolved" }>
|
||||
| UserVisibleNoticeNotification;
|
||||
|
||||
export function conversationRuntimeEventsFromNotification(
|
||||
export function turnRuntimeEventsFromNotification(
|
||||
notification: RuntimeEventSource,
|
||||
localItemId: (prefix: string) => string,
|
||||
): readonly ConversationRuntimeEvent[] {
|
||||
): readonly TurnRuntimeEvent[] {
|
||||
switch (notification.method) {
|
||||
case "item/agentMessage/delta":
|
||||
return [
|
||||
{
|
||||
type: "assistantDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
delta: notification.params.delta,
|
||||
completeReasoning: true,
|
||||
},
|
||||
|
|
@ -53,7 +49,7 @@ export function conversationRuntimeEventsFromNotification(
|
|||
{
|
||||
type: "planDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
delta: notification.params.delta,
|
||||
},
|
||||
];
|
||||
|
|
@ -61,7 +57,7 @@ export function conversationRuntimeEventsFromNotification(
|
|||
return [
|
||||
{
|
||||
type: "itemUpserted",
|
||||
item: taskProgressMessageStreamItem(notification.params.turnId, notification.params.explanation, notification.params.plan),
|
||||
item: taskProgressThreadStreamItem(notification.params.turnId, notification.params.explanation, notification.params.plan),
|
||||
},
|
||||
];
|
||||
case "item/reasoning/summaryTextDelta":
|
||||
|
|
@ -70,7 +66,7 @@ export function conversationRuntimeEventsFromNotification(
|
|||
{
|
||||
type: "textDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
label: "reasoning",
|
||||
delta: notification.params.delta,
|
||||
kind: "reasoning",
|
||||
|
|
@ -81,7 +77,7 @@ export function conversationRuntimeEventsFromNotification(
|
|||
{
|
||||
type: "textDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
label: "reasoning",
|
||||
delta: "",
|
||||
kind: "reasoning",
|
||||
|
|
@ -96,7 +92,7 @@ export function conversationRuntimeEventsFromNotification(
|
|||
{
|
||||
type: "itemOutputDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
delta: notification.params.delta,
|
||||
kind: "command",
|
||||
fallbackText: STREAMED_COMMAND_RUNNING_TEXT,
|
||||
|
|
@ -114,14 +110,14 @@ export function conversationRuntimeEventsFromNotification(
|
|||
{
|
||||
type: "itemOutputDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
delta: notification.params.delta,
|
||||
kind: "fileChange",
|
||||
fallbackText: STREAMED_FILE_CHANGE_IN_PROGRESS_TEXT,
|
||||
},
|
||||
];
|
||||
case "turn/diff/updated":
|
||||
return [{ type: "turnDiffUpdated", runId: notification.params.turnId, diff: notification.params.diff }];
|
||||
return [{ type: "turnDiffUpdated", turnId: notification.params.turnId, diff: notification.params.diff }];
|
||||
case "hook/started":
|
||||
return hookRunEvents(notification.params.run, notification.params.turnId, "running");
|
||||
case "hook/completed":
|
||||
|
|
@ -131,7 +127,7 @@ export function conversationRuntimeEventsFromNotification(
|
|||
{
|
||||
type: "toolOutputDelta",
|
||||
itemId: notification.params.itemId,
|
||||
runId: notification.params.turnId,
|
||||
turnId: notification.params.turnId,
|
||||
delta: notification.params.message,
|
||||
fallbackLabel: STREAMED_MCP_PROGRESS_LABEL,
|
||||
},
|
||||
|
|
@ -144,20 +140,20 @@ export function conversationRuntimeEventsFromNotification(
|
|||
case "turn/started":
|
||||
return [
|
||||
{
|
||||
type: "runStarted",
|
||||
type: "turnStarted",
|
||||
threadId: notification.params.threadId,
|
||||
runId: notification.params.turn.id,
|
||||
turnId: notification.params.turn.id,
|
||||
recencyAt: notification.params.turn.startedAt,
|
||||
},
|
||||
];
|
||||
case "turn/completed":
|
||||
return [
|
||||
{
|
||||
type: "runCompleted",
|
||||
type: "turnCompleted",
|
||||
threadId: notification.params.threadId,
|
||||
runId: notification.params.turn.id,
|
||||
turnId: notification.params.turn.id,
|
||||
status: notification.params.turn.status,
|
||||
completedItems: messageStreamItemsFromTurns([notification.params.turn]),
|
||||
completedItems: threadStreamItemsFromTurns([notification.params.turn]),
|
||||
completedSummary: completedConversationSummaryFromAppServerTurn(notification.params.turn),
|
||||
},
|
||||
];
|
||||
|
|
@ -177,35 +173,35 @@ export function conversationRuntimeEventsFromNotification(
|
|||
}
|
||||
}
|
||||
|
||||
function startedItemEvents(item: AppServerTurnItem, runId: string): readonly ConversationRuntimeEvent[] {
|
||||
function startedItemEvents(item: AppServerTurnItem, turnId: string): readonly TurnRuntimeEvent[] {
|
||||
if (shouldSuppressLifecycleItem(item)) return [];
|
||||
const streamItem = messageStreamItemFromTurnItem(item, runId);
|
||||
const streamItem = threadStreamItemFromTurnItem(item, turnId);
|
||||
return streamItem ? [{ type: "itemUpserted", item: streamItem }] : [];
|
||||
}
|
||||
|
||||
function completedItemEvents(item: AppServerTurnItem, runId: string): readonly ConversationRuntimeEvent[] {
|
||||
function completedItemEvents(item: AppServerTurnItem, turnId: string): readonly TurnRuntimeEvent[] {
|
||||
if (item.type === "userMessage") return [];
|
||||
const streamItem = messageStreamItemFromTurnItem(item, runId);
|
||||
return streamItem ? [{ type: "itemCompleted", runId, item: streamItem }] : [];
|
||||
const streamItem = threadStreamItemFromTurnItem(item, turnId);
|
||||
return streamItem ? [{ type: "itemCompleted", turnId, item: streamItem }] : [];
|
||||
}
|
||||
|
||||
function fileChangeItem(itemId: string, runId: string, changes: readonly AppServerFileChange[], status: string): MessageStreamItem {
|
||||
return streamingFileChangeMessageStreamItem(itemId, runId, normalizeFileChanges(changes), status);
|
||||
function fileChangeItem(itemId: string, turnId: string, changes: readonly AppServerFileChange[], status: string): ThreadStreamItem {
|
||||
return streamingFileChangeThreadStreamItem(itemId, turnId, normalizeFileChanges(changes), status);
|
||||
}
|
||||
|
||||
function hookRunEvents(
|
||||
run: Extract<ServerNotification, { method: "hook/started" }>["params"]["run"],
|
||||
runId: string | null,
|
||||
turnId: string | null,
|
||||
status: string,
|
||||
): readonly ConversationRuntimeEvent[] {
|
||||
const item = hookRunMessageStreamItem(run, runId, status);
|
||||
return item ? [{ type: "hookRunObserved", item, runId, eventName: run.eventName }] : [];
|
||||
): readonly TurnRuntimeEvent[] {
|
||||
const item = hookRunThreadStreamItem(run, turnId, status);
|
||||
return item ? [{ type: "hookRunObserved", item, turnId, eventName: run.eventName }] : [];
|
||||
}
|
||||
|
||||
function jsonNoticeEvent(
|
||||
notification: Extract<UserVisibleNoticeNotification, { method: Exclude<UserVisibleNoticeNotification["method"], "thread/compacted"> }>,
|
||||
localItemId: (prefix: string) => string,
|
||||
): ConversationRuntimeEvent {
|
||||
): TurnRuntimeEvent {
|
||||
return {
|
||||
type: "systemNotice",
|
||||
item: createSystemItem(localItemId("system"), `${notification.method}: ${jsonPreview(notification.params)}`),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AgentMessageStreamItem, AgentStateSummary, ExecutionState } from "../../../domain/message-stream/items";
|
||||
import type { AgentStateSummary, AgentThreadStreamItem, ExecutionState } from "../../../domain/thread-stream/items";
|
||||
import {
|
||||
collabAgentStateExecutionState,
|
||||
type ExecutionStateByStatus,
|
||||
|
|
@ -12,7 +12,7 @@ const STANDARD_TOOL_STATES: ExecutionStateByStatus = {
|
|||
failed: "failed",
|
||||
};
|
||||
|
||||
interface MessageStreamCollabAgentToolCall {
|
||||
interface ThreadStreamCollabAgentToolCall {
|
||||
id: string;
|
||||
tool: string;
|
||||
status: string;
|
||||
|
|
@ -21,15 +21,15 @@ interface MessageStreamCollabAgentToolCall {
|
|||
prompt: string | null;
|
||||
model: string | null;
|
||||
reasoningEffort: string | null;
|
||||
agentsStates: Record<string, MessageStreamCollabAgentState | undefined>;
|
||||
agentsStates: Record<string, ThreadStreamCollabAgentState | undefined>;
|
||||
}
|
||||
|
||||
interface MessageStreamCollabAgentState {
|
||||
interface ThreadStreamCollabAgentState {
|
||||
status?: string | null;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
export function agentMessageStreamItem(item: MessageStreamCollabAgentToolCall, turnId?: string): AgentMessageStreamItem {
|
||||
export function agentThreadStreamItem(item: ThreadStreamCollabAgentToolCall, turnId?: string): AgentThreadStreamItem {
|
||||
const agents = agentStatesDisplay(item.agentsStates);
|
||||
return {
|
||||
id: item.id,
|
||||
|
|
@ -49,7 +49,7 @@ export function agentMessageStreamItem(item: MessageStreamCollabAgentToolCall, t
|
|||
};
|
||||
}
|
||||
|
||||
function agentStatesDisplay(states: MessageStreamCollabAgentToolCall["agentsStates"]): AgentStateSummary[] {
|
||||
function agentStatesDisplay(states: ThreadStreamCollabAgentToolCall["agentsStates"]): AgentStateSummary[] {
|
||||
return Object.entries(states)
|
||||
.map(([threadId, state]) => {
|
||||
const status = state?.status ?? "unknown";
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { type MessageStreamExecutionState, RUNNING_EXECUTION_STATE } from "../../../domain/message-stream/execution-state";
|
||||
import type { ExecutionState } from "../../../domain/message-stream/items";
|
||||
import { RUNNING_EXECUTION_STATE, type ThreadStreamExecutionState } from "../../../domain/thread-stream/execution-state";
|
||||
import type { ExecutionState } from "../../../domain/thread-stream/items";
|
||||
|
||||
export { RUNNING_EXECUTION_STATE };
|
||||
|
||||
export type ExecutionStateByStatus = Readonly<Record<string, MessageStreamExecutionState>>;
|
||||
export type ExecutionStateByStatus = Readonly<Record<string, ThreadStreamExecutionState>>;
|
||||
|
||||
const COMMAND_STATES = {
|
||||
inProgress: RUNNING_EXECUTION_STATE,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MessageStreamFileChange, MessageStreamItem } from "../../../domain/message-stream/items";
|
||||
import type { ThreadStreamFileChange, ThreadStreamItem } from "../../../domain/thread-stream/items";
|
||||
import { patchApplyExecutionState } from "./execution-state";
|
||||
|
||||
export interface AppServerFileChange {
|
||||
|
|
@ -9,7 +9,7 @@ export interface AppServerFileChange {
|
|||
readonly diff: string;
|
||||
}
|
||||
|
||||
export function normalizeFileChanges(changes: readonly AppServerFileChange[]): MessageStreamFileChange[] {
|
||||
export function normalizeFileChanges(changes: readonly AppServerFileChange[]): ThreadStreamFileChange[] {
|
||||
return changes.map((change) => ({
|
||||
kind: change.kind.type,
|
||||
path: change.path,
|
||||
|
|
@ -17,12 +17,12 @@ export function normalizeFileChanges(changes: readonly AppServerFileChange[]): M
|
|||
}));
|
||||
}
|
||||
|
||||
export function streamingFileChangeMessageStreamItem(
|
||||
export function streamingFileChangeThreadStreamItem(
|
||||
itemId: string,
|
||||
turnId: string,
|
||||
changes: readonly MessageStreamFileChange[],
|
||||
changes: readonly ThreadStreamFileChange[],
|
||||
status: string,
|
||||
): MessageStreamItem {
|
||||
): ThreadStreamItem {
|
||||
return {
|
||||
id: itemId,
|
||||
kind: "fileChange",
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { HookMessageStreamItem } from "../../../domain/message-stream/items";
|
||||
import type { HookThreadStreamItem } from "../../../domain/thread-stream/items";
|
||||
import { type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE } from "./execution-state";
|
||||
|
||||
interface MessageStreamHookRun {
|
||||
interface ThreadStreamHookRun {
|
||||
id: string;
|
||||
eventName: string | null;
|
||||
statusMessage: string | null;
|
||||
|
|
@ -18,7 +18,7 @@ const HOOK_RUN_STATES: ExecutionStateByStatus = {
|
|||
stopped: "failed",
|
||||
};
|
||||
|
||||
export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: string | null, status: string): HookMessageStreamItem | null {
|
||||
export function hookRunThreadStreamItem(run: ThreadStreamHookRun, turnId: string | null, status: string): HookThreadStreamItem | null {
|
||||
if (run.id.length === 0) return null;
|
||||
const trimmedEventName = run.eventName?.trim();
|
||||
const eventName = trimmedEventName && trimmedEventName.length > 0 ? trimmedEventName : "Hook";
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { jsonPreview } from "../../../../../domain/display/json-preview";
|
||||
import type { MessageStreamAuditFact } from "../../../domain/message-stream/items";
|
||||
import type { ThreadStreamAuditFact } from "../../../domain/thread-stream/items";
|
||||
|
||||
export interface AutoReviewPermissionProfile {
|
||||
network?: { enabled?: boolean | null } | null;
|
||||
|
|
@ -22,8 +22,8 @@ type AutoReviewFileSystemPath =
|
|||
| { kind: string };
|
||||
};
|
||||
|
||||
export function autoReviewPermissionRows(permissions: AutoReviewPermissionProfile): MessageStreamAuditFact[] {
|
||||
const rows: MessageStreamAuditFact[] = [];
|
||||
export function autoReviewPermissionRows(permissions: AutoReviewPermissionProfile): ThreadStreamAuditFact[] {
|
||||
const rows: ThreadStreamAuditFact[] = [];
|
||||
const networkEnabled = permissions.network?.enabled;
|
||||
if (typeof networkEnabled === "boolean") {
|
||||
rows.push({ key: "network", value: networkEnabled ? "enabled" : "disabled" });
|
||||
|
|
@ -45,7 +45,7 @@ export function autoReviewPermissionRows(permissions: AutoReviewPermissionProfil
|
|||
return rows;
|
||||
}
|
||||
|
||||
function addOptional(rows: MessageStreamAuditFact[], key: string, value: unknown): void {
|
||||
function addOptional(rows: ThreadStreamAuditFact[], key: string, value: unknown): void {
|
||||
if (value === null || value === undefined) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
rows.push({ key, value: stringValue(value) });
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { pathRelativeToRoot } from "../../../../../domain/vault/paths";
|
||||
import type { ExecutionState, MessageStreamAuditFact, MessageStreamItem } from "../../../domain/message-stream/items";
|
||||
import type { ExecutionState, ThreadStreamAuditFact, ThreadStreamItem } from "../../../domain/thread-stream/items";
|
||||
import { type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE } from "./execution-state";
|
||||
import { type AutoReviewPermissionProfile, autoReviewPermissionRows } from "./permission-rows";
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ type AutoReviewAction =
|
|||
}
|
||||
| { type: "requestPermissions"; reason: string | null; permissions: AutoReviewPermissionProfile };
|
||||
|
||||
export function createReviewResultItem(id: string, text: string): MessageStreamItem {
|
||||
export function createReviewResultItem(id: string, text: string): ThreadStreamItem {
|
||||
const parsed = parseAutomaticApprovalReviewMessage(text);
|
||||
if (parsed) {
|
||||
return {
|
||||
|
|
@ -72,7 +72,7 @@ export function createReviewResultItem(id: string, text: string): MessageStreamI
|
|||
};
|
||||
}
|
||||
|
||||
export function createAutoReviewResultItem(params: AutoReviewNotification): MessageStreamItem {
|
||||
export function createAutoReviewResultItem(params: AutoReviewNotification): ThreadStreamItem {
|
||||
const completed = "decisionSource" in params;
|
||||
const status = params.review.status;
|
||||
const action = autoReviewActionLabel(params.action);
|
||||
|
|
@ -124,7 +124,7 @@ function parseAutomaticApprovalReviewMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function autoReviewActionRows(action: AutoReviewAction): MessageStreamAuditFact[] {
|
||||
function autoReviewActionRows(action: AutoReviewAction): ThreadStreamAuditFact[] {
|
||||
if (action.type === "command") {
|
||||
return [
|
||||
{ key: "action", value: "command" },
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MessageStreamItem } from "../../../domain/message-stream/items";
|
||||
import type { ThreadStreamItem } from "../../../domain/thread-stream/items";
|
||||
import { type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE } from "./execution-state";
|
||||
|
||||
const TASK_STATES = {
|
||||
|
|
@ -14,11 +14,7 @@ interface TaskPlanStep {
|
|||
status: TaskStepStatus;
|
||||
}
|
||||
|
||||
export function taskProgressMessageStreamItem(
|
||||
turnId: string,
|
||||
explanation: string | null,
|
||||
plan: readonly TaskPlanStep[],
|
||||
): MessageStreamItem {
|
||||
export function taskProgressThreadStreamItem(turnId: string, explanation: string | null, plan: readonly TaskPlanStep[]): ThreadStreamItem {
|
||||
const trimmedExplanation = explanation?.trim();
|
||||
const status = plan.some((step) => step.status === "inProgress" || step.status === "pending") ? "inProgress" : "completed";
|
||||
return {
|
||||
|
|
@ -8,12 +8,12 @@ import { jsonPreview } from "../../../../../domain/display/json-preview";
|
|||
import type { HistoricalTurn } from "../../../../../domain/threads/history";
|
||||
import { referencedThreadMetadataFromPrompt } from "../../../../../domain/threads/reference";
|
||||
import type { ThreadConversationSummary } from "../../../../../domain/threads/transcript";
|
||||
import { fileMentionsFromInput } from "../../../domain/message-stream/format/file-mentions";
|
||||
import { normalizeProposedPlanMarkdown } from "../../../domain/message-stream/format/proposed-plan";
|
||||
import { userMessageDisplayText } from "../../../domain/message-stream/format/user-message-text";
|
||||
import type { CommandMessageStreamTarget, MessageStreamDiagnosticSection, MessageStreamItem } from "../../../domain/message-stream/items";
|
||||
import type { MessageStreamItemProvenance } from "../../../domain/message-stream/provenance";
|
||||
import { agentMessageStreamItem } from "./agent-items";
|
||||
import { fileMentionsFromInput } from "../../../domain/thread-stream/format/file-mentions";
|
||||
import { normalizeProposedPlanMarkdown } from "../../../domain/thread-stream/format/proposed-plan";
|
||||
import { userMessageDisplayText } from "../../../domain/thread-stream/format/user-message-text";
|
||||
import type { CommandThreadStreamTarget, ThreadStreamDiagnosticSection, ThreadStreamItem } from "../../../domain/thread-stream/items";
|
||||
import type { ThreadStreamItemProvenance } from "../../../domain/thread-stream/provenance";
|
||||
import { agentThreadStreamItem } from "./agent-items";
|
||||
import {
|
||||
appServerFailedStatusLabel,
|
||||
commandExecutionState,
|
||||
|
|
@ -52,67 +52,67 @@ export function completedConversationSummaryFromAppServerTurn(turn: TurnRecord):
|
|||
return completedConversationSummaryFromTurnRecord(turn);
|
||||
}
|
||||
|
||||
export function messageStreamItemsFromTurns(turns: readonly HistoricalTurn[]): MessageStreamItem[] {
|
||||
export function threadStreamItemsFromTurns(turns: readonly HistoricalTurn[]): ThreadStreamItem[] {
|
||||
const sortedTurns = [...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
|
||||
const items: MessageStreamItem[] = [];
|
||||
const items: ThreadStreamItem[] = [];
|
||||
for (const turn of sortedTurns) {
|
||||
for (const item of turn.items as readonly TurnItem[]) {
|
||||
const streamItem = messageStreamItemFromTurnItem(item, turn.id);
|
||||
const streamItem = threadStreamItemFromTurnItem(item, turn.id);
|
||||
if (streamItem) items.push(streamItem);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export function messageStreamItemFromTurnItem(item: TurnItem, turnId?: string): MessageStreamItem | null {
|
||||
const streamItem = messageStreamItemFromTurnItemCore(item, turnId);
|
||||
export function threadStreamItemFromTurnItem(item: TurnItem, turnId?: string): ThreadStreamItem | null {
|
||||
const streamItem = threadStreamItemFromTurnItemCore(item, turnId);
|
||||
return streamItem ? withTurnItemProvenance(streamItem, item) : null;
|
||||
}
|
||||
|
||||
function messageStreamItemFromTurnItemCore(item: TurnItem, turnId?: string): MessageStreamItem | null {
|
||||
function threadStreamItemFromTurnItemCore(item: TurnItem, turnId?: string): ThreadStreamItem | null {
|
||||
switch (item.type) {
|
||||
case "userMessage":
|
||||
return userMessageStreamItem(item, turnId);
|
||||
return userThreadStreamItem(item, turnId);
|
||||
case "agentMessage":
|
||||
return assistantMessageStreamItemFromTurn(item, turnId);
|
||||
return assistantThreadStreamItemFromTurn(item, turnId);
|
||||
case "commandExecution":
|
||||
return commandMessageStreamItem(item, turnId);
|
||||
return commandThreadStreamItem(item, turnId);
|
||||
case "fileChange":
|
||||
return fileChangeMessageStreamItem(item, turnId);
|
||||
return fileChangeThreadStreamItem(item, turnId);
|
||||
case "plan":
|
||||
return proposedPlanMessageStreamItem(item, turnId);
|
||||
return proposedPlanThreadStreamItem(item, turnId);
|
||||
case "hookPrompt":
|
||||
return hookPromptMessageStreamItem(item, turnId);
|
||||
return hookPromptThreadStreamItem(item, turnId);
|
||||
case "reasoning":
|
||||
return reasoningMessageStreamItem(item, turnId);
|
||||
return reasoningThreadStreamItem(item, turnId);
|
||||
case "mcpToolCall":
|
||||
return mcpToolCallMessageStreamItem(item, turnId);
|
||||
return mcpToolCallThreadStreamItem(item, turnId);
|
||||
case "dynamicToolCall":
|
||||
return dynamicToolCallMessageStreamItem(item, turnId);
|
||||
return dynamicToolCallThreadStreamItem(item, turnId);
|
||||
case "collabAgentToolCall":
|
||||
return agentMessageStreamItem(item, turnId);
|
||||
return agentThreadStreamItem(item, turnId);
|
||||
case "webSearch":
|
||||
return webSearchMessageStreamItem(item, turnId);
|
||||
return webSearchThreadStreamItem(item, turnId);
|
||||
case "imageView":
|
||||
return imageViewMessageStreamItem(item, turnId);
|
||||
return imageViewThreadStreamItem(item, turnId);
|
||||
case "sleep":
|
||||
return sleepMessageStreamItem(item, turnId);
|
||||
return sleepThreadStreamItem(item, turnId);
|
||||
case "imageGeneration":
|
||||
return imageGenerationMessageStreamItem(item, turnId);
|
||||
return imageGenerationThreadStreamItem(item, turnId);
|
||||
case "subAgentActivity":
|
||||
return null;
|
||||
case "enteredReviewMode":
|
||||
case "exitedReviewMode":
|
||||
return reviewModeMessageStreamItem(item, turnId);
|
||||
return reviewModeThreadStreamItem(item, turnId);
|
||||
case "contextCompaction":
|
||||
return contextCompactionMessageStreamItem(item, turnId);
|
||||
return contextCompactionThreadStreamItem(item, turnId);
|
||||
default:
|
||||
return ignoredUnsupportedTurnItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
function withTurnItemProvenance(item: MessageStreamItem, turnItem: TurnItem): MessageStreamItem {
|
||||
const provenance: MessageStreamItemProvenance = {
|
||||
function withTurnItemProvenance(item: ThreadStreamItem, turnItem: TurnItem): ThreadStreamItem {
|
||||
const provenance: ThreadStreamItemProvenance = {
|
||||
source: "appServer",
|
||||
channel: "turnItem",
|
||||
itemType: turnItem.type,
|
||||
|
|
@ -129,15 +129,15 @@ function turnItemSourceFields(item: { id: string }, turnId?: string): TurnItemSo
|
|||
};
|
||||
}
|
||||
|
||||
function userMessageStreamItem(item: UserMessageItem, turnId?: string): MessageStreamItem {
|
||||
function userThreadStreamItem(item: UserMessageItem, turnId?: string): ThreadStreamItem {
|
||||
const text = turnUserItemText(item);
|
||||
const referencedThread = referencedThreadMetadataFromPrompt(text);
|
||||
const mentionedFiles = fileMentionsFromInput(item.content);
|
||||
if (referencedThread) {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "message",
|
||||
messageKind: "user",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "user",
|
||||
role: "user",
|
||||
text: userMessageDisplayText(referencedThread.text, item.content),
|
||||
copyText: referencedThread.text,
|
||||
|
|
@ -148,8 +148,8 @@ function userMessageStreamItem(item: UserMessageItem, turnId?: string): MessageS
|
|||
}
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "message",
|
||||
messageKind: "user",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "user",
|
||||
role: "user",
|
||||
text: userMessageDisplayText(text, item.content),
|
||||
copyText: text,
|
||||
|
|
@ -158,32 +158,32 @@ function userMessageStreamItem(item: UserMessageItem, turnId?: string): MessageS
|
|||
};
|
||||
}
|
||||
|
||||
function assistantMessageStreamItemFromTurn(item: AgentMessageItem, turnId?: string): MessageStreamItem {
|
||||
function assistantThreadStreamItemFromTurn(item: AgentMessageItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "message",
|
||||
messageKind: "assistantResponse",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "assistantResponse",
|
||||
role: "assistant",
|
||||
text: item.text,
|
||||
copyText: item.text,
|
||||
messageState: "completed",
|
||||
dialogueState: "completed",
|
||||
};
|
||||
}
|
||||
|
||||
function proposedPlanMessageStreamItem(item: PlanItem, turnId?: string): MessageStreamItem {
|
||||
function proposedPlanThreadStreamItem(item: PlanItem, turnId?: string): ThreadStreamItem {
|
||||
const text = normalizeProposedPlanMarkdown(item.text);
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "message",
|
||||
messageKind: "proposedPlan",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "proposedPlan",
|
||||
role: "assistant",
|
||||
text,
|
||||
copyText: text,
|
||||
messageState: "completed",
|
||||
dialogueState: "completed",
|
||||
};
|
||||
}
|
||||
|
||||
function hookPromptMessageStreamItem(item: HookPromptItem, turnId?: string): MessageStreamItem {
|
||||
function hookPromptThreadStreamItem(item: HookPromptItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "hook",
|
||||
|
|
@ -192,7 +192,7 @@ function hookPromptMessageStreamItem(item: HookPromptItem, turnId?: string): Mes
|
|||
};
|
||||
}
|
||||
|
||||
function reasoningMessageStreamItem(item: ReasoningItem, turnId?: string): MessageStreamItem {
|
||||
function reasoningThreadStreamItem(item: ReasoningItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "reasoning",
|
||||
|
|
@ -201,7 +201,7 @@ function reasoningMessageStreamItem(item: ReasoningItem, turnId?: string): Messa
|
|||
};
|
||||
}
|
||||
|
||||
function mcpToolCallMessageStreamItem(item: McpToolCallItem, turnId?: string): MessageStreamItem {
|
||||
function mcpToolCallThreadStreamItem(item: McpToolCallItem, turnId?: string): ThreadStreamItem {
|
||||
const name = `${item.server}.${item.tool}`;
|
||||
const target = jsonTargetLabel(item.arguments);
|
||||
return {
|
||||
|
|
@ -225,7 +225,7 @@ function mcpToolCallMessageStreamItem(item: McpToolCallItem, turnId?: string): M
|
|||
};
|
||||
}
|
||||
|
||||
function dynamicToolCallMessageStreamItem(item: DynamicToolCallItem, turnId?: string): MessageStreamItem {
|
||||
function dynamicToolCallThreadStreamItem(item: DynamicToolCallItem, turnId?: string): ThreadStreamItem {
|
||||
const name = `${item.namespace ? `${item.namespace}.` : ""}${item.tool}`;
|
||||
const target = jsonTargetLabel(item.arguments);
|
||||
const failure = item.success === false ? "failed" : appServerFailedStatusLabel(item.status);
|
||||
|
|
@ -246,7 +246,7 @@ function dynamicToolCallMessageStreamItem(item: DynamicToolCallItem, turnId?: st
|
|||
};
|
||||
}
|
||||
|
||||
function webSearchMessageStreamItem(item: WebSearchItem, turnId?: string): MessageStreamItem {
|
||||
function webSearchThreadStreamItem(item: WebSearchItem, turnId?: string): ThreadStreamItem {
|
||||
const target = webSearchTarget(item);
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
|
|
@ -260,7 +260,7 @@ function webSearchMessageStreamItem(item: WebSearchItem, turnId?: string): Messa
|
|||
};
|
||||
}
|
||||
|
||||
function imageViewMessageStreamItem(item: ImageViewItem, turnId?: string): MessageStreamItem {
|
||||
function imageViewThreadStreamItem(item: ImageViewItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "tool",
|
||||
|
|
@ -270,7 +270,7 @@ function imageViewMessageStreamItem(item: ImageViewItem, turnId?: string): Messa
|
|||
};
|
||||
}
|
||||
|
||||
function sleepMessageStreamItem(item: SleepItem, turnId?: string): MessageStreamItem {
|
||||
function sleepThreadStreamItem(item: SleepItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "wait",
|
||||
|
|
@ -280,7 +280,7 @@ function sleepMessageStreamItem(item: SleepItem, turnId?: string): MessageStream
|
|||
};
|
||||
}
|
||||
|
||||
function imageGenerationMessageStreamItem(item: ImageGenerationItem, turnId?: string): MessageStreamItem {
|
||||
function imageGenerationThreadStreamItem(item: ImageGenerationItem, turnId?: string): ThreadStreamItem {
|
||||
const target = item.savedPath ?? item.result;
|
||||
const failureReason = appServerFailedStatusLabel(item.status);
|
||||
return {
|
||||
|
|
@ -303,7 +303,7 @@ function imageGenerationMessageStreamItem(item: ImageGenerationItem, turnId?: st
|
|||
};
|
||||
}
|
||||
|
||||
function reviewModeMessageStreamItem(item: ReviewModeItem, turnId?: string): MessageStreamItem {
|
||||
function reviewModeThreadStreamItem(item: ReviewModeItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "tool",
|
||||
|
|
@ -314,7 +314,7 @@ function reviewModeMessageStreamItem(item: ReviewModeItem, turnId?: string): Mes
|
|||
};
|
||||
}
|
||||
|
||||
function contextCompactionMessageStreamItem(item: ContextCompactionItem, turnId?: string): MessageStreamItem {
|
||||
function contextCompactionThreadStreamItem(item: ContextCompactionItem, turnId?: string): ThreadStreamItem {
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
kind: "contextCompaction",
|
||||
|
|
@ -329,7 +329,7 @@ function reasoningText(item: ReasoningItem): string {
|
|||
.join("\n\n");
|
||||
}
|
||||
|
||||
function commandTarget(item: CommandExecutionItem): CommandMessageStreamTarget {
|
||||
function commandTarget(item: CommandExecutionItem): CommandThreadStreamTarget {
|
||||
const action = representativeCommandAction(item.commandActions);
|
||||
if (action?.type === "search") {
|
||||
const query = commandActionValue(action.query) ?? undefined;
|
||||
|
|
@ -421,7 +421,7 @@ function webSearchQueryList(
|
|||
return unique.join("; ");
|
||||
}
|
||||
|
||||
function webSearchDetails(item: WebSearchItem): Extract<MessageStreamItem, { kind: "tool" }>["webSearch"] {
|
||||
function webSearchDetails(item: WebSearchItem): Extract<ThreadStreamItem, { kind: "tool" }>["webSearch"] {
|
||||
const details: {
|
||||
action?: string;
|
||||
query?: string;
|
||||
|
|
@ -444,7 +444,7 @@ function webSearchDetails(item: WebSearchItem): Extract<MessageStreamItem, { kin
|
|||
return Object.keys(details).length > 0 ? details : undefined;
|
||||
}
|
||||
|
||||
function commandMessageStreamItem(item: CommandExecutionItem, turnId?: string): MessageStreamItem {
|
||||
function commandThreadStreamItem(item: CommandExecutionItem, turnId?: string): ThreadStreamItem {
|
||||
const exitCode = typeof item.exitCode === "number" ? item.exitCode : undefined;
|
||||
const durationMs = typeof item.durationMs === "number" ? item.durationMs : undefined;
|
||||
return {
|
||||
|
|
@ -463,7 +463,7 @@ function commandMessageStreamItem(item: CommandExecutionItem, turnId?: string):
|
|||
};
|
||||
}
|
||||
|
||||
function fileChangeMessageStreamItem(item: FileChangeItem, turnId?: string): MessageStreamItem {
|
||||
function fileChangeThreadStreamItem(item: FileChangeItem, turnId?: string): ThreadStreamItem {
|
||||
const changes = normalizeFileChanges(item.changes);
|
||||
return {
|
||||
...turnItemSourceFields(item, turnId),
|
||||
|
|
@ -519,7 +519,7 @@ function jsonTargetLabel(value: unknown): string | null {
|
|||
|
||||
function jsonDiagnosticSections(
|
||||
...sections: readonly { readonly title: string; readonly value: unknown }[]
|
||||
): readonly MessageStreamDiagnosticSection[] | undefined {
|
||||
): readonly ThreadStreamDiagnosticSection[] | undefined {
|
||||
const diagnostics = sections
|
||||
.filter((section) => section.value !== null && section.value !== undefined)
|
||||
.map((section) => ({ title: section.title, body: jsonPreview(section.value) }));
|
||||
|
|
@ -4,7 +4,7 @@ import { shortThreadId } from "../../../domain/threads/id";
|
|||
import type { Thread } from "../../../domain/threads/model";
|
||||
import { REFERENCED_THREAD_TURN_LIMIT, referencedThreadPromptBundle } from "../../../domain/threads/reference";
|
||||
import type { ComposerInputSnapshot } from "../application/composer/input-snapshot";
|
||||
import type { ThreadReferenceInput } from "../application/conversation/slash-command-execution";
|
||||
import type { ThreadReferenceInput } from "../application/turns/slash-command-execution";
|
||||
|
||||
interface ThreadReferenceResolverHost {
|
||||
currentClient(): ThreadConversationSummaryClient | null;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
import { interruptTurn, startTurn, steerTurn } from "../../../../app-server/services/turns";
|
||||
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
|
||||
import type { ThreadTurnsPage } from "../../../../domain/threads/history";
|
||||
import type { ChatTurnTransport } from "../../application/conversation/turn-transport";
|
||||
import type { RuntimeSettingsTransport } from "../../application/runtime/settings-transport";
|
||||
import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../../application/threads/goal-transport";
|
||||
import type {
|
||||
|
|
@ -28,7 +27,8 @@ import type {
|
|||
} from "../../application/threads/thread-loading-transport";
|
||||
import type { ThreadMutationTransport, ThreadRollbackSnapshot } from "../../application/threads/thread-mutation-transport";
|
||||
import type { ThreadStartTransport } from "../../application/threads/thread-start-transport";
|
||||
import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items";
|
||||
import type { ChatTurnTransport } from "../../application/turns/turn-transport";
|
||||
import { threadStreamItemsFromTurns } from "../mappers/thread-stream/turn-items";
|
||||
|
||||
interface CurrentChatAppServerClientHost {
|
||||
currentClient(): AppServerClient | null;
|
||||
|
|
@ -161,7 +161,7 @@ function createChatThreadMutationTransport(host: ChatAppServerTransportHost): Th
|
|||
return {
|
||||
thread: snapshot.thread,
|
||||
cwd: snapshot.cwd,
|
||||
items: messageStreamItemsFromTurns(snapshot.turns),
|
||||
items: threadStreamItemsFromTurns(snapshot.turns),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -235,7 +235,7 @@ async function readChatThreadHistoryPage(
|
|||
|
||||
function chatThreadHistoryPageFromTurnsPage(page: ThreadTurnsPage): ThreadHistoryPage {
|
||||
return {
|
||||
items: messageStreamItemsFromTurns(page.turns),
|
||||
items: threadStreamItemsFromTurns(page.turns),
|
||||
nextCursor: page.nextCursor,
|
||||
hadTurns: page.turns.length > 0,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,237 +0,0 @@
|
|||
import { reconcileCompletedTurnItems } from "../../domain/message-stream/completed-turn-reconciliation";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { attachHookRunsToTurn, completeReasoningItems, upsertMessageStreamItemById } from "../../domain/message-stream/updates";
|
||||
import { messageStreamItems } from "../state/message-stream";
|
||||
import { type ChatAction, type ChatState, chatReducer } from "../state/root-reducer";
|
||||
import type { ConversationRuntimeEvent } from "./runtime-events";
|
||||
import { activeTurnId, pendingTurnStart as pendingTurnStartForState } from "./turn-state";
|
||||
|
||||
export type ConversationRuntimeOutcome =
|
||||
| { type: "run-started"; threadId: string; runId: string; recencyAt: number | null }
|
||||
| { type: "run-completed"; threadId: string; runId: string; completedSummary: ConversationRuntimeEventCompletedSummary };
|
||||
|
||||
type ConversationRuntimeEventCompletedSummary = Extract<ConversationRuntimeEvent, { type: "runCompleted" }>["completedSummary"];
|
||||
|
||||
export interface ConversationRuntimePlan {
|
||||
actions: readonly ChatAction[];
|
||||
outcomes: readonly ConversationRuntimeOutcome[];
|
||||
}
|
||||
|
||||
const EMPTY_PLAN: ConversationRuntimePlan = { actions: [], outcomes: [] };
|
||||
|
||||
export function planConversationRuntimeEvents(state: ChatState, events: readonly ConversationRuntimeEvent[]): ConversationRuntimePlan {
|
||||
let currentState = state;
|
||||
const actions: ChatAction[] = [];
|
||||
const outcomes: ConversationRuntimeOutcome[] = [];
|
||||
for (const event of events) {
|
||||
const plan = planConversationRuntimeEvent(currentState, event);
|
||||
actions.push(...plan.actions);
|
||||
outcomes.push(...plan.outcomes);
|
||||
currentState = reducePlannedActions(currentState, plan.actions);
|
||||
}
|
||||
return actions.length === 0 && outcomes.length === 0 ? EMPTY_PLAN : { actions, outcomes };
|
||||
}
|
||||
|
||||
function planConversationRuntimeEvent(state: ChatState, event: ConversationRuntimeEvent): ConversationRuntimePlan {
|
||||
switch (event.type) {
|
||||
case "assistantDelta":
|
||||
return actionPlan({
|
||||
type: "message-stream/assistant-delta-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.runId,
|
||||
delta: event.delta,
|
||||
completeReasoning: event.completeReasoning,
|
||||
});
|
||||
case "planDelta":
|
||||
return actionPlan({
|
||||
type: "message-stream/plan-delta-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.runId,
|
||||
delta: event.delta,
|
||||
});
|
||||
case "textDelta":
|
||||
return actionPlan({
|
||||
type: "message-stream/item-text-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.runId,
|
||||
label: event.label,
|
||||
delta: event.delta,
|
||||
kind: event.kind,
|
||||
});
|
||||
case "toolOutputDelta":
|
||||
return actionPlan({
|
||||
type: "message-stream/tool-output-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.runId,
|
||||
delta: event.delta,
|
||||
fallbackLabel: event.fallbackLabel,
|
||||
});
|
||||
case "itemOutputDelta":
|
||||
return actionPlan({
|
||||
type: "message-stream/item-output-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.runId,
|
||||
delta: event.delta,
|
||||
kind: event.kind,
|
||||
fallbackText: event.fallbackText,
|
||||
});
|
||||
case "itemUpserted":
|
||||
return actionPlan({ type: "message-stream/item-upserted", item: event.item });
|
||||
case "itemCompleted":
|
||||
return completedItemPlan(event.item, event.runId);
|
||||
case "autoReviewUpdated":
|
||||
return autoReviewUpdatedPlan(state, event.item);
|
||||
case "runStarted":
|
||||
return runStartedPlan(state, event);
|
||||
case "runCompleted":
|
||||
return runCompletedPlan(state, event);
|
||||
case "turnDiffUpdated":
|
||||
return actionPlan({ type: "message-stream/turn-diff-updated", turnId: event.runId, diff: event.diff });
|
||||
case "hookRunObserved":
|
||||
return hookRunPlan(state, event);
|
||||
case "requestResolved":
|
||||
return actionPlan({ type: "request/resolved", requestId: event.requestId });
|
||||
case "reviewWarning":
|
||||
return reviewWarningPlan(state, event.item);
|
||||
case "systemNotice":
|
||||
return actionPlan({ type: "message-stream/system-item-added", item: event.item });
|
||||
}
|
||||
}
|
||||
|
||||
function runStartedPlan(state: ChatState, event: Extract<ConversationRuntimeEvent, { type: "runStarted" }>): ConversationRuntimePlan {
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
type: "turn/started",
|
||||
threadId: event.threadId,
|
||||
turnId: event.runId,
|
||||
items: messageStreamItemsWithPendingPromptSubmitHooks(state, event.runId),
|
||||
},
|
||||
],
|
||||
outcomes: [
|
||||
{
|
||||
type: "run-started",
|
||||
threadId: event.threadId,
|
||||
runId: event.runId,
|
||||
recencyAt: event.recencyAt,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function runCompletedPlan(state: ChatState, event: Extract<ConversationRuntimeEvent, { type: "runCompleted" }>): ConversationRuntimePlan {
|
||||
if (activeTurnId(state) !== event.runId) return EMPTY_PLAN;
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
type: "turn/completed",
|
||||
turnId: event.runId,
|
||||
status: event.status,
|
||||
items: completeReasoningItems(
|
||||
reconcileCompletedTurnItems({
|
||||
currentItems: messageStreamItems(state.messageStream),
|
||||
completedTurnId: event.runId,
|
||||
turnItems: event.completedItems,
|
||||
}),
|
||||
event.runId,
|
||||
),
|
||||
},
|
||||
],
|
||||
outcomes: [
|
||||
{
|
||||
type: "run-completed",
|
||||
threadId: event.threadId,
|
||||
runId: event.runId,
|
||||
completedSummary: event.completedSummary,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function completedItemPlan(item: MessageStreamItem, runId: string): ConversationRuntimePlan {
|
||||
return {
|
||||
actions: [
|
||||
{ type: "message-stream/item-upserted", item },
|
||||
...(item.kind === "reasoning" ? ([{ type: "message-stream/reasoning-completed", turnId: runId }] satisfies ChatAction[]) : []),
|
||||
],
|
||||
outcomes: [],
|
||||
};
|
||||
}
|
||||
|
||||
function hookRunPlan(state: ChatState, event: Extract<ConversationRuntimeEvent, { type: "hookRunObserved" }>): ConversationRuntimePlan {
|
||||
const resolvedRunId = hookRunId(state, event);
|
||||
const item = resolvedRunId ? { ...event.item, turnId: resolvedRunId } : event.item;
|
||||
const currentPendingTurnStart = pendingTurnStartForState(state);
|
||||
let pendingTurnStart = currentPendingTurnStart;
|
||||
if (!resolvedRunId && currentPendingTurnStart && event.eventName === "userPromptSubmit") {
|
||||
const hookIds = currentPendingTurnStart.promptSubmitHookItemIds;
|
||||
pendingTurnStart = hookIds.includes(item.id)
|
||||
? currentPendingTurnStart
|
||||
: { ...currentPendingTurnStart, promptSubmitHookItemIds: [...hookIds, item.id] };
|
||||
}
|
||||
return actionPlan({
|
||||
type: "turn/pending-start-hook-upserted",
|
||||
item,
|
||||
pendingTurnStart,
|
||||
});
|
||||
}
|
||||
|
||||
function hookRunId(state: ChatState, event: Extract<ConversationRuntimeEvent, { type: "hookRunObserved" }>): string | null {
|
||||
if (event.runId) return event.runId;
|
||||
if (event.eventName === "userPromptSubmit" && !pendingTurnStartForState(state)) return activeTurnId(state);
|
||||
return null;
|
||||
}
|
||||
|
||||
function reviewWarningPlan(state: ChatState, item: MessageStreamItem): ConversationRuntimePlan {
|
||||
if (
|
||||
isUnstructuredAutoReviewWarning(item) &&
|
||||
hasStructuredAutoReviewResult(messageStreamItems(state.messageStream), activeTurnId(state))
|
||||
) {
|
||||
return EMPTY_PLAN;
|
||||
}
|
||||
return actionPlan({ type: "message-stream/item-upserted", item });
|
||||
}
|
||||
|
||||
function autoReviewUpdatedPlan(state: ChatState, item: MessageStreamItem): ConversationRuntimePlan {
|
||||
return actionPlan({
|
||||
type: "message-stream/items-replaced",
|
||||
items: upsertMessageStreamItemById(
|
||||
messageStreamItems(state.messageStream).filter((currentItem) => !isUnstructuredAutoReviewWarning(currentItem)),
|
||||
item,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function messageStreamItemsWithPendingPromptSubmitHooks(state: ChatState, runId: string): readonly MessageStreamItem[] {
|
||||
const pending = pendingTurnStartForState(state);
|
||||
const items = messageStreamItems(state.messageStream);
|
||||
if (!pending) return items;
|
||||
return attachHookRunsToTurn(items, runId, pending.promptSubmitHookItemIds, pending.anchorItemId);
|
||||
}
|
||||
|
||||
function hasStructuredAutoReviewResult(items: readonly MessageStreamItem[], activeRunId: string | null): boolean {
|
||||
return items.some(
|
||||
(item) =>
|
||||
item.kind === "reviewResult" && Boolean(item.turnId) && (!activeRunId || item.turnId === activeRunId) && isAutoReviewText(item.text),
|
||||
);
|
||||
}
|
||||
|
||||
function isUnstructuredAutoReviewWarning(item: MessageStreamItem): boolean {
|
||||
return item.kind === "reviewResult" && !item.turnId && isAutoReviewText(item.text);
|
||||
}
|
||||
|
||||
function isAutoReviewText(text: string): boolean {
|
||||
return /^Auto-review\b/i.test(text.trim());
|
||||
}
|
||||
|
||||
function reducePlannedActions(state: ChatState, actions: readonly ChatAction[]): ChatState {
|
||||
return actions.reduce(reducePlannedAction, state);
|
||||
}
|
||||
|
||||
function reducePlannedAction(state: ChatState, action: ChatAction): ChatState {
|
||||
return chatReducer(state, action);
|
||||
}
|
||||
|
||||
function actionPlan(action: ChatAction): ConversationRuntimePlan {
|
||||
return { actions: [action], outcomes: [] };
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { activeThreadRuntimeState, pendingRuntimeIntentState } from "../../domain/runtime/state";
|
||||
import { messageStreamItems } from "../state/message-stream";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import type { ChatState } from "../state/root-reducer";
|
||||
import { threadStreamItems } from "../state/thread-stream";
|
||||
|
||||
interface RuntimeSnapshotInput {
|
||||
runtimeConfig: ChatState["connection"]["runtimeConfig"];
|
||||
|
|
@ -13,7 +13,7 @@ interface RuntimeSnapshotInput {
|
|||
availableModels: ChatState["connection"]["availableModels"];
|
||||
}
|
||||
|
||||
export function messageItemsHaveThreadTurns(items: readonly MessageStreamItem[]): boolean {
|
||||
export function threadStreamItemsHaveThreadTurns(items: readonly ThreadStreamItem[]): boolean {
|
||||
return items.some((item) => item.turnId);
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export function runtimeSnapshotForChatState(state: ChatState): RuntimeSnapshot {
|
|||
activeThread: state.activeThread,
|
||||
runtime: state.runtime,
|
||||
rateLimit: state.connection.rateLimit,
|
||||
hasThreadTurns: messageItemsHaveThreadTurns(messageStreamItems(state.messageStream)),
|
||||
hasThreadTurns: threadStreamItemsHaveThreadTurns(threadStreamItems(state.threadStream)),
|
||||
availableModels: state.connection.availableModels,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ import { parseServiceTier, type ServiceTier } from "../../../../domain/runtime/p
|
|||
import type { ServerInitialization } from "../../../../domain/server/initialization";
|
||||
import type { ThreadActivationSnapshot } from "../../../../domain/threads/activation";
|
||||
import { type Thread, upsertThread } from "../../../../domain/threads/model";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import type { CollaborationModeSelection } from "../../domain/runtime/intent";
|
||||
import type { ActiveThreadRuntimeState } from "../../domain/runtime/state";
|
||||
import type { PendingTurnStart } from "../conversation/turn-state";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import type { PendingTurnStart } from "../turns/turn-state";
|
||||
|
||||
interface ResumedThreadActionParams {
|
||||
response: ThreadActivationSnapshot;
|
||||
listedThreads?: readonly Thread[];
|
||||
items?: readonly MessageStreamItem[];
|
||||
items?: readonly ThreadStreamItem[];
|
||||
preserveRequestedRuntimeSettings?: boolean;
|
||||
serviceTierKnown?: boolean;
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ interface ResumedThreadFromActiveRuntimeParams {
|
|||
| "activePermissionProfile"
|
||||
>;
|
||||
listedThreads?: readonly Thread[];
|
||||
items?: readonly MessageStreamItem[];
|
||||
items?: readonly ThreadStreamItem[];
|
||||
}
|
||||
|
||||
export interface ActiveThreadResumedAction extends RuntimePermissionState, RuntimePermissionKnownState {
|
||||
|
|
@ -51,7 +51,7 @@ export interface ActiveThreadResumedAction extends RuntimePermissionState, Runti
|
|||
serviceTier: ServiceTier | null;
|
||||
serviceTierKnown?: boolean;
|
||||
approvalsReviewer: ActiveThreadRuntimeState["approvalsReviewer"];
|
||||
items?: readonly MessageStreamItem[];
|
||||
items?: readonly ThreadStreamItem[];
|
||||
status?: string;
|
||||
listedThreads?: readonly Thread[];
|
||||
preserveRequestedRuntimeSettings?: boolean;
|
||||
|
|
@ -108,19 +108,19 @@ export interface DisclosureSetAction {
|
|||
|
||||
export interface TurnOptimisticStartedAction {
|
||||
type: "turn/optimistic-started";
|
||||
item: MessageStreamItem;
|
||||
item: ThreadStreamItem;
|
||||
pendingTurnStart: PendingTurnStart;
|
||||
}
|
||||
|
||||
export interface TurnStartAcknowledgedAction {
|
||||
type: "turn/start-acknowledged";
|
||||
turnId: string;
|
||||
items: readonly MessageStreamItem[];
|
||||
items: readonly ThreadStreamItem[];
|
||||
}
|
||||
|
||||
export interface TurnStartFailedAction {
|
||||
type: "turn/start-failed";
|
||||
items: readonly MessageStreamItem[];
|
||||
items: readonly ThreadStreamItem[];
|
||||
}
|
||||
|
||||
export function resumedThreadActionFromActiveRuntime(params: ResumedThreadFromActiveRuntimeParams): ActiveThreadResumedAction {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { createServerDiagnostics } from "../../../../domain/server/diagnostics";
|
|||
import type { ServerInitialization } from "../../../../domain/server/initialization";
|
||||
import type { ThreadGoal } from "../../../../domain/threads/goal";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { type CollaborationModeSelection, type RequestedFastMode, unchangedCollaborationModeIntent } from "../../domain/runtime/intent";
|
||||
import {
|
||||
type ChatRuntimeState,
|
||||
|
|
@ -29,14 +28,8 @@ import {
|
|||
resetReasoningEffortToConfigRuntimeState,
|
||||
setSelectedCollaborationModeRuntimeState,
|
||||
} from "../../domain/runtime/state";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import type { ComposerSuggestion } from "../composer/suggestions";
|
||||
import {
|
||||
type ChatTurnState,
|
||||
initialChatTurnState,
|
||||
type PendingTurnStart,
|
||||
STATUS_TURN_RUNNING,
|
||||
transitionChatTurnLifecycleState,
|
||||
} from "../conversation/turn-state";
|
||||
import {
|
||||
type ChatRequestState,
|
||||
initialChatRequestState,
|
||||
|
|
@ -45,6 +38,13 @@ import {
|
|||
reduceRequestSlice,
|
||||
resolveChatRequest,
|
||||
} from "../pending-requests/state";
|
||||
import {
|
||||
type ChatTurnState,
|
||||
initialChatTurnState,
|
||||
type PendingTurnStart,
|
||||
STATUS_TURN_RUNNING,
|
||||
transitionChatTurnLifecycleState,
|
||||
} from "../turns/turn-state";
|
||||
import type {
|
||||
ActiveThreadResumedAction,
|
||||
ActiveThreadSettingsAppliedAction,
|
||||
|
|
@ -57,18 +57,18 @@ import type {
|
|||
TurnStartAcknowledgedAction,
|
||||
TurnStartFailedAction,
|
||||
} from "./actions";
|
||||
import {
|
||||
type ChatMessageStreamState,
|
||||
initialChatMessageStreamState,
|
||||
isMessageStreamAction,
|
||||
type MessageStreamAction,
|
||||
messageStreamItems,
|
||||
messageStreamStartActiveSegment,
|
||||
messageStreamWithActiveTurnItems,
|
||||
messageStreamWithItems,
|
||||
reduceMessageStreamSlice,
|
||||
} from "./message-stream";
|
||||
import { definedPatch, patchObject } from "./patch";
|
||||
import {
|
||||
type ChatThreadStreamState,
|
||||
initialChatThreadStreamState,
|
||||
isThreadStreamAction,
|
||||
reduceThreadStreamSlice,
|
||||
type ThreadStreamAction,
|
||||
threadStreamItems,
|
||||
threadStreamStartActiveSegment,
|
||||
threadStreamWithActiveTurnItems,
|
||||
threadStreamWithItems,
|
||||
} from "./thread-stream";
|
||||
import {
|
||||
type ChatUiState,
|
||||
clearAllRequestDisclosures,
|
||||
|
|
@ -124,7 +124,7 @@ interface ChatStateShape {
|
|||
activeThread: ChatActiveThreadState;
|
||||
runtime: ChatRuntimeState;
|
||||
turn: ChatTurnState;
|
||||
messageStream: ChatMessageStreamState;
|
||||
threadStream: ChatThreadStreamState;
|
||||
requests: ChatRequestState;
|
||||
composer: ChatComposerState;
|
||||
ui: ChatUiState;
|
||||
|
|
@ -169,14 +169,14 @@ interface TurnStartedAction {
|
|||
type: "turn/started";
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
items?: readonly MessageStreamItem[];
|
||||
items?: readonly ThreadStreamItem[];
|
||||
}
|
||||
|
||||
interface TurnCompletedAction {
|
||||
type: "turn/completed";
|
||||
turnId: string;
|
||||
status: string;
|
||||
items: readonly MessageStreamItem[];
|
||||
items: readonly ThreadStreamItem[];
|
||||
}
|
||||
|
||||
type TurnAction =
|
||||
|
|
@ -213,12 +213,12 @@ export type ChatAction = ChatTransitionAction | ChatSliceAction;
|
|||
interface RequestResolvedAction {
|
||||
type: "request/resolved";
|
||||
requestId: PendingRequestId;
|
||||
resultItem?: MessageStreamItem;
|
||||
resultItem?: ThreadStreamItem;
|
||||
}
|
||||
|
||||
interface PendingStartHookUpsertedAction {
|
||||
type: "turn/pending-start-hook-upserted";
|
||||
item: MessageStreamItem;
|
||||
item: ThreadStreamItem;
|
||||
pendingTurnStart: PendingTurnStart | null;
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ type ChatSliceAction =
|
|||
| ActiveThreadAction
|
||||
| RuntimeAction
|
||||
| RequestAction
|
||||
| MessageStreamAction
|
||||
| ThreadStreamAction
|
||||
| ComposerAction
|
||||
| UiAction;
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ export function createChatState(): ChatState {
|
|||
activeThread: initialActiveThreadState(),
|
||||
runtime: initialChatRuntimeState(),
|
||||
turn: initialTurnState(),
|
||||
messageStream: initialMessageStreamState(),
|
||||
threadStream: initialThreadStreamState(),
|
||||
requests: initialRequestState(),
|
||||
composer: initialComposerState(),
|
||||
ui: initialUiState(),
|
||||
|
|
@ -344,7 +344,7 @@ function reduceActiveThreadResumedTransition(state: ChatState, action: ActiveThr
|
|||
},
|
||||
},
|
||||
turn: initialTurnState(),
|
||||
messageStream: initialMessageStreamState(action.items ?? []),
|
||||
threadStream: initialThreadStreamState(action.items ?? []),
|
||||
requests: initialRequestState(),
|
||||
composer: initialComposerState(),
|
||||
ui: initialUiState(),
|
||||
|
|
@ -397,9 +397,9 @@ function reduceTurnStartedTransition(state: ChatState, action: TurnStartedAction
|
|||
activeThread: { ...state.activeThread, id: action.threadId },
|
||||
turn: { lifecycle },
|
||||
connection: { ...state.connection, statusText: STATUS_TURN_RUNNING },
|
||||
messageStream: action.items
|
||||
? messageStreamWithActiveTurnItems(state.messageStream, action.turnId, action.items)
|
||||
: messageStreamStartActiveSegment(state.messageStream, action.turnId, []),
|
||||
threadStream: action.items
|
||||
? threadStreamWithActiveTurnItems(state.threadStream, action.turnId, action.items)
|
||||
: threadStreamStartActiveSegment(state.threadStream, action.turnId, []),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +408,7 @@ function reduceTurnCompletedTransition(state: ChatState, action: TurnCompletedAc
|
|||
if (lifecycle === state.turn.lifecycle) return state;
|
||||
return patchChatState(state, {
|
||||
turn: { lifecycle },
|
||||
messageStream: messageStreamWithItems(state.messageStream, action.items),
|
||||
threadStream: threadStreamWithItems(state.threadStream, action.items),
|
||||
connection: { ...state.connection, statusText: `Turn ${action.status}.` },
|
||||
});
|
||||
}
|
||||
|
|
@ -420,7 +420,7 @@ function reduceTurnOptimisticStartedTransition(state: ChatState, action: TurnOpt
|
|||
});
|
||||
return patchChatState(state, {
|
||||
turn: { lifecycle },
|
||||
messageStream: messageStreamStartActiveSegment(state.messageStream, null, [action.item]),
|
||||
threadStream: threadStreamStartActiveSegment(state.threadStream, null, [action.item]),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +432,7 @@ function reduceTurnStartAcknowledgedTransition(state: ChatState, action: TurnSta
|
|||
if (lifecycle === state.turn.lifecycle) return state;
|
||||
return patchChatState(state, {
|
||||
turn: { lifecycle },
|
||||
messageStream: messageStreamWithActiveTurnItems(state.messageStream, action.turnId, action.items),
|
||||
threadStream: threadStreamWithActiveTurnItems(state.threadStream, action.turnId, action.items),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -441,13 +441,13 @@ function reduceTurnStartFailedTransition(state: ChatState, action: TurnStartFail
|
|||
if (lifecycle === state.turn.lifecycle) return state;
|
||||
return patchChatState(state, {
|
||||
turn: { lifecycle },
|
||||
messageStream: messageStreamWithItems(state.messageStream, action.items),
|
||||
threadStream: threadStreamWithItems(state.threadStream, action.items),
|
||||
});
|
||||
}
|
||||
|
||||
function reducePendingStartHookUpsertedTransition(state: ChatState, action: PendingStartHookUpsertedAction): ChatState {
|
||||
return patchChatState(state, {
|
||||
messageStream: reduceMessageStreamSlice(state.messageStream, { type: "message-stream/item-upserted", item: action.item }),
|
||||
threadStream: reduceThreadStreamSlice(state.threadStream, { type: "thread-stream/item-upserted", item: action.item }),
|
||||
turn: {
|
||||
lifecycle: transitionChatTurnLifecycleState(state.turn.lifecycle, {
|
||||
type: "pending-start-hook-upserted",
|
||||
|
|
@ -463,9 +463,9 @@ function reduceRequestResolvedTransition(state: ChatState, action: RequestResolv
|
|||
return patchChatState(state, {
|
||||
requests,
|
||||
ui: clearResolvedRequestDisclosures(state.ui, action.requestId),
|
||||
messageStream: action.resultItem
|
||||
? reduceMessageStreamSlice(state.messageStream, { type: "message-stream/item-added", item: action.resultItem })
|
||||
: state.messageStream,
|
||||
threadStream: action.resultItem
|
||||
? reduceThreadStreamSlice(state.threadStream, { type: "thread-stream/item-added", item: action.resultItem })
|
||||
: state.threadStream,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +474,7 @@ function clearTurnScopedState(state: ChatState): ChatState {
|
|||
turn: {
|
||||
lifecycle: transitionChatTurnLifecycleState(state.turn.lifecycle, { type: "cleared" }),
|
||||
},
|
||||
messageStream: messageStreamWithItems(state.messageStream, messageStreamItems(state.messageStream)),
|
||||
threadStream: threadStreamWithItems(state.threadStream, threadStreamItems(state.threadStream)),
|
||||
requests: initialRequestState(),
|
||||
ui: clearAllRequestDisclosures(state.ui),
|
||||
});
|
||||
|
|
@ -485,7 +485,7 @@ function clearThreadScopedState(state: ChatState): ChatState {
|
|||
patchChatState(state, {
|
||||
activeThread: initialActiveThreadState(),
|
||||
runtime: initialChatRuntimeState(),
|
||||
messageStream: initialMessageStreamState(),
|
||||
threadStream: initialThreadStreamState(),
|
||||
composer: initialComposerState(),
|
||||
ui: initialUiState(),
|
||||
}),
|
||||
|
|
@ -516,7 +516,7 @@ function reduceChatSlices(state: ChatState, action: ChatSliceAction): ChatState
|
|||
runtime: reduceRuntimeSlice(state.runtime, action),
|
||||
turn: state.turn,
|
||||
requests: isRequestAction(action) ? reduceRequestSlice(state.requests, action) : state.requests,
|
||||
messageStream: isMessageStreamAction(action) ? reduceMessageStreamSlice(state.messageStream, action) : state.messageStream,
|
||||
threadStream: isThreadStreamAction(action) ? reduceThreadStreamSlice(state.threadStream, action) : state.threadStream,
|
||||
composer: reduceComposerSlice(state.composer, action),
|
||||
ui: isUiAction(action) ? reduceUiSlice(state.ui, action) : state.ui,
|
||||
});
|
||||
|
|
@ -656,8 +656,8 @@ function initialTurnState(): ChatTurnState {
|
|||
return initialChatTurnState();
|
||||
}
|
||||
|
||||
function initialMessageStreamState(items: readonly MessageStreamItem[] = []): ChatMessageStreamState {
|
||||
return initialChatMessageStreamState(items);
|
||||
function initialThreadStreamState(items: readonly ThreadStreamItem[] = []): ChatThreadStreamState {
|
||||
return initialChatThreadStreamState(items);
|
||||
}
|
||||
|
||||
function initialRequestState(): ChatRequestState {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ChatMessageStreamActiveSegment, ChatMessageStreamState } from "./message-stream";
|
||||
import { type ChatAction, type ChatState, chatReducer, createChatState } from "./root-reducer";
|
||||
import type { ChatThreadStreamActiveSegment, ChatThreadStreamState } from "./thread-stream";
|
||||
import { cloneDisclosureUiState } from "./ui-state";
|
||||
|
||||
export interface ChatStateStore {
|
||||
|
|
@ -44,7 +44,7 @@ function cloneChatState(state: ChatState): ChatState {
|
|||
activeThread: { ...state.activeThread },
|
||||
runtime: { ...state.runtime },
|
||||
turn: { lifecycle: state.turn.lifecycle },
|
||||
messageStream: cloneMessageStreamState(state.messageStream),
|
||||
threadStream: cloneThreadStreamState(state.threadStream),
|
||||
requests: {
|
||||
approvals: [...state.requests.approvals],
|
||||
pendingUserInputs: [...state.requests.pendingUserInputs],
|
||||
|
|
@ -67,7 +67,7 @@ function cloneChatState(state: ChatState): ChatState {
|
|||
};
|
||||
}
|
||||
|
||||
function cloneMessageStreamState(state: ChatMessageStreamState): ChatMessageStreamState {
|
||||
function cloneThreadStreamState(state: ChatThreadStreamState): ChatThreadStreamState {
|
||||
return {
|
||||
stableItems: [...state.stableItems],
|
||||
activeSegment: cloneActiveSegment(state.activeSegment),
|
||||
|
|
@ -78,7 +78,7 @@ function cloneMessageStreamState(state: ChatMessageStreamState): ChatMessageStre
|
|||
};
|
||||
}
|
||||
|
||||
function cloneActiveSegment(segment: ChatMessageStreamActiveSegment | null): ChatMessageStreamActiveSegment | null {
|
||||
function cloneActiveSegment(segment: ChatThreadStreamActiveSegment | null): ChatThreadStreamActiveSegment | null {
|
||||
if (!segment) return null;
|
||||
return {
|
||||
turnId: segment.turnId,
|
||||
|
|
|
|||
|
|
@ -1,54 +1,54 @@
|
|||
import {
|
||||
streamedItemOutputMessageStreamItem,
|
||||
streamedTextMessageStreamItem,
|
||||
streamedToolOutputMessageStreamItem,
|
||||
} from "../../domain/message-stream/factories/streaming-items";
|
||||
import { normalizeProposedPlanMarkdown } from "../../domain/message-stream/format/proposed-plan";
|
||||
import type { MessageStreamItem, MessageStreamMessageItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamSemanticClassifications } from "../../domain/message-stream/semantics/classify";
|
||||
import { messageStreamIsTurnInitiator } from "../../domain/message-stream/semantics/predicates";
|
||||
import { completeReasoningItems, upsertMessageStreamItemById } from "../../domain/message-stream/updates";
|
||||
streamedItemOutputThreadStreamItem,
|
||||
streamedTextThreadStreamItem,
|
||||
streamedToolOutputThreadStreamItem,
|
||||
} from "../../domain/thread-stream/factories/streaming-items";
|
||||
import { normalizeProposedPlanMarkdown } from "../../domain/thread-stream/format/proposed-plan";
|
||||
import type { ThreadStreamDialogueItem, ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { threadStreamSemanticClassifications } from "../../domain/thread-stream/semantics/classify";
|
||||
import { threadStreamIsTurnInitiator } from "../../domain/thread-stream/semantics/predicates";
|
||||
import { completeReasoningItems, upsertThreadStreamItemById } from "../../domain/thread-stream/updates";
|
||||
import { definedPatch, patchObject } from "./patch";
|
||||
|
||||
export interface ChatMessageStreamActiveSegment {
|
||||
export interface ChatThreadStreamActiveSegment {
|
||||
readonly turnId: string | null;
|
||||
readonly items: readonly MessageStreamItem[];
|
||||
readonly items: readonly ThreadStreamItem[];
|
||||
readonly indexById: ReadonlyMap<string, number>;
|
||||
readonly indexBySourceItemId: ReadonlyMap<string, number>;
|
||||
}
|
||||
|
||||
export interface ChatMessageStreamState {
|
||||
readonly stableItems: readonly MessageStreamItem[];
|
||||
readonly activeSegment: ChatMessageStreamActiveSegment | null;
|
||||
export interface ChatThreadStreamState {
|
||||
readonly stableItems: readonly ThreadStreamItem[];
|
||||
readonly activeSegment: ChatThreadStreamActiveSegment | null;
|
||||
readonly turnDiffs: ReadonlyMap<string, string>;
|
||||
readonly historyCursor: string | null;
|
||||
readonly loadingHistory: boolean;
|
||||
readonly reportedLogs: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export interface MessageStreamRollbackCandidate {
|
||||
export interface ThreadStreamRollbackCandidate {
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type MessageStreamAction =
|
||||
| { type: "message-stream/item-added"; item: MessageStreamItem }
|
||||
| { type: "message-stream/system-item-added"; item: MessageStreamItem }
|
||||
| { type: "message-stream/deduped-log-added"; text: string; item: MessageStreamItem }
|
||||
| { type: "message-stream/history-loading-set"; loading: boolean }
|
||||
export type ThreadStreamAction =
|
||||
| { type: "thread-stream/item-added"; item: ThreadStreamItem }
|
||||
| { type: "thread-stream/system-item-added"; item: ThreadStreamItem }
|
||||
| { type: "thread-stream/deduped-log-added"; text: string; item: ThreadStreamItem }
|
||||
| { type: "thread-stream/history-loading-set"; loading: boolean }
|
||||
| {
|
||||
type: "message-stream/items-replaced";
|
||||
items: readonly MessageStreamItem[];
|
||||
type: "thread-stream/items-replaced";
|
||||
items: readonly ThreadStreamItem[];
|
||||
historyCursor?: string | null;
|
||||
loadingHistory?: boolean;
|
||||
}
|
||||
| { type: "message-stream/item-upserted"; item: MessageStreamItem }
|
||||
| { type: "message-stream/reasoning-completed"; turnId: string }
|
||||
| { type: "message-stream/assistant-delta-appended"; itemId: string; turnId: string; delta: string; completeReasoning?: boolean }
|
||||
| { type: "message-stream/plan-delta-appended"; itemId: string; turnId: string; delta: string }
|
||||
| { type: "thread-stream/item-upserted"; item: ThreadStreamItem }
|
||||
| { type: "thread-stream/reasoning-completed"; turnId: string }
|
||||
| { type: "thread-stream/assistant-delta-appended"; itemId: string; turnId: string; delta: string; completeReasoning?: boolean }
|
||||
| { type: "thread-stream/plan-delta-appended"; itemId: string; turnId: string; delta: string }
|
||||
| {
|
||||
type: "message-stream/item-text-appended";
|
||||
type: "thread-stream/item-text-appended";
|
||||
itemId: string;
|
||||
turnId: string;
|
||||
label: string;
|
||||
|
|
@ -56,44 +56,44 @@ export type MessageStreamAction =
|
|||
kind: "tool" | "hook" | "reasoning";
|
||||
}
|
||||
| {
|
||||
type: "message-stream/tool-output-appended";
|
||||
type: "thread-stream/tool-output-appended";
|
||||
itemId: string;
|
||||
turnId: string;
|
||||
delta: string;
|
||||
fallbackLabel: string;
|
||||
}
|
||||
| {
|
||||
type: "message-stream/item-output-appended";
|
||||
type: "thread-stream/item-output-appended";
|
||||
itemId: string;
|
||||
turnId: string;
|
||||
delta: string;
|
||||
kind: "command" | "fileChange";
|
||||
fallbackText: string;
|
||||
}
|
||||
| { type: "message-stream/turn-diff-updated"; turnId: string; diff: string };
|
||||
| { type: "thread-stream/turn-diff-updated"; turnId: string; diff: string };
|
||||
|
||||
export function isMessageStreamAction(action: { type: string }): action is MessageStreamAction {
|
||||
export function isThreadStreamAction(action: { type: string }): action is ThreadStreamAction {
|
||||
switch (action.type) {
|
||||
case "message-stream/item-added":
|
||||
case "message-stream/system-item-added":
|
||||
case "message-stream/deduped-log-added":
|
||||
case "message-stream/history-loading-set":
|
||||
case "message-stream/items-replaced":
|
||||
case "message-stream/item-upserted":
|
||||
case "message-stream/reasoning-completed":
|
||||
case "message-stream/assistant-delta-appended":
|
||||
case "message-stream/plan-delta-appended":
|
||||
case "message-stream/item-text-appended":
|
||||
case "message-stream/tool-output-appended":
|
||||
case "message-stream/item-output-appended":
|
||||
case "message-stream/turn-diff-updated":
|
||||
case "thread-stream/item-added":
|
||||
case "thread-stream/system-item-added":
|
||||
case "thread-stream/deduped-log-added":
|
||||
case "thread-stream/history-loading-set":
|
||||
case "thread-stream/items-replaced":
|
||||
case "thread-stream/item-upserted":
|
||||
case "thread-stream/reasoning-completed":
|
||||
case "thread-stream/assistant-delta-appended":
|
||||
case "thread-stream/plan-delta-appended":
|
||||
case "thread-stream/item-text-appended":
|
||||
case "thread-stream/tool-output-appended":
|
||||
case "thread-stream/item-output-appended":
|
||||
case "thread-stream/turn-diff-updated":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function initialChatMessageStreamState(items: readonly MessageStreamItem[] = []): ChatMessageStreamState {
|
||||
export function initialChatThreadStreamState(items: readonly ThreadStreamItem[] = []): ChatThreadStreamState {
|
||||
return {
|
||||
stableItems: items,
|
||||
activeSegment: null,
|
||||
|
|
@ -104,39 +104,39 @@ export function initialChatMessageStreamState(items: readonly MessageStreamItem[
|
|||
};
|
||||
}
|
||||
|
||||
export function messageStreamItems(state: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">): readonly MessageStreamItem[] {
|
||||
export function threadStreamItems(state: Pick<ChatThreadStreamState, "stableItems" | "activeSegment">): readonly ThreadStreamItem[] {
|
||||
if (!state.activeSegment || state.activeSegment.items.length === 0) return state.stableItems;
|
||||
return [...state.stableItems, ...state.activeSegment.items];
|
||||
}
|
||||
|
||||
export function messageStreamStableItems(state: Pick<ChatMessageStreamState, "stableItems">): readonly MessageStreamItem[] {
|
||||
export function threadStreamStableItems(state: Pick<ChatThreadStreamState, "stableItems">): readonly ThreadStreamItem[] {
|
||||
return state.stableItems;
|
||||
}
|
||||
|
||||
export function messageStreamActiveItems(state: Pick<ChatMessageStreamState, "activeSegment">): readonly MessageStreamItem[] {
|
||||
export function threadStreamActiveItems(state: Pick<ChatThreadStreamState, "activeSegment">): readonly ThreadStreamItem[] {
|
||||
return state.activeSegment?.items ?? [];
|
||||
}
|
||||
|
||||
export function messageStreamIsEmpty(state: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">): boolean {
|
||||
export function threadStreamIsEmpty(state: Pick<ChatThreadStreamState, "stableItems" | "activeSegment">): boolean {
|
||||
return state.stableItems.length === 0 && (!state.activeSegment || state.activeSegment.items.length === 0);
|
||||
}
|
||||
|
||||
export function messageStreamTurnsAfterTurnId(
|
||||
state: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">,
|
||||
export function threadStreamTurnsAfterTurnId(
|
||||
state: Pick<ChatThreadStreamState, "stableItems" | "activeSegment">,
|
||||
turnId: string,
|
||||
): number | null {
|
||||
const turnIds = orderedTurnIds(messageStreamItems(state));
|
||||
const turnIds = orderedTurnIds(threadStreamItems(state));
|
||||
const index = turnIds.indexOf(turnId);
|
||||
return index === -1 ? null : turnIds.length - index - 1;
|
||||
}
|
||||
|
||||
export function messageStreamRollbackCandidate(
|
||||
state: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">,
|
||||
): MessageStreamRollbackCandidate | null {
|
||||
return messageStreamRollbackCandidateFromItems(messageStreamItems(state));
|
||||
export function threadStreamRollbackCandidate(
|
||||
state: Pick<ChatThreadStreamState, "stableItems" | "activeSegment">,
|
||||
): ThreadStreamRollbackCandidate | null {
|
||||
return threadStreamRollbackCandidateFromItems(threadStreamItems(state));
|
||||
}
|
||||
|
||||
export function messageStreamRollbackCandidateFromItems(items: readonly MessageStreamItem[]): MessageStreamRollbackCandidate | null {
|
||||
export function threadStreamRollbackCandidateFromItems(items: readonly ThreadStreamItem[]): ThreadStreamRollbackCandidate | null {
|
||||
const lastTurnId = latestTurnId(items);
|
||||
if (!lastTurnId) return null;
|
||||
|
||||
|
|
@ -150,11 +150,11 @@ export function messageStreamRollbackCandidateFromItems(items: readonly MessageS
|
|||
};
|
||||
}
|
||||
|
||||
export function messageStreamWithItems(
|
||||
state: ChatMessageStreamState,
|
||||
items: readonly MessageStreamItem[],
|
||||
patch: Partial<Pick<ChatMessageStreamState, "historyCursor" | "loadingHistory">> = {},
|
||||
): ChatMessageStreamState {
|
||||
export function threadStreamWithItems(
|
||||
state: ChatThreadStreamState,
|
||||
items: readonly ThreadStreamItem[],
|
||||
patch: Partial<Pick<ChatThreadStreamState, "historyCursor" | "loadingHistory">> = {},
|
||||
): ChatThreadStreamState {
|
||||
return patchObject(state, {
|
||||
stableItems: items,
|
||||
activeSegment: null,
|
||||
|
|
@ -162,11 +162,11 @@ export function messageStreamWithItems(
|
|||
});
|
||||
}
|
||||
|
||||
export function messageStreamWithActiveTurnItems(
|
||||
state: ChatMessageStreamState,
|
||||
export function threadStreamWithActiveTurnItems(
|
||||
state: ChatThreadStreamState,
|
||||
turnId: string,
|
||||
items: readonly MessageStreamItem[],
|
||||
): ChatMessageStreamState {
|
||||
items: readonly ThreadStreamItem[],
|
||||
): ChatThreadStreamState {
|
||||
const stableItems = items.filter((item) => item.turnId !== turnId);
|
||||
const activeItems = items.filter((item) => item.turnId === turnId);
|
||||
return patchObject(state, {
|
||||
|
|
@ -175,158 +175,158 @@ export function messageStreamWithActiveTurnItems(
|
|||
});
|
||||
}
|
||||
|
||||
export function messageStreamStartActiveSegment(
|
||||
state: ChatMessageStreamState,
|
||||
export function threadStreamStartActiveSegment(
|
||||
state: ChatThreadStreamState,
|
||||
turnId: string | null,
|
||||
items: readonly MessageStreamItem[],
|
||||
): ChatMessageStreamState {
|
||||
items: readonly ThreadStreamItem[],
|
||||
): ChatThreadStreamState {
|
||||
return patchObject(state, { activeSegment: activeSegmentFromItems(turnId, items) });
|
||||
}
|
||||
|
||||
export function reduceMessageStreamSlice(state: ChatMessageStreamState, action: MessageStreamAction): ChatMessageStreamState {
|
||||
export function reduceThreadStreamSlice(state: ChatThreadStreamState, action: ThreadStreamAction): ChatThreadStreamState {
|
||||
switch (action.type) {
|
||||
case "message-stream/item-added":
|
||||
case "message-stream/system-item-added":
|
||||
return patchObject(state, appendMessageStreamItemPatch(state, action.item));
|
||||
case "message-stream/deduped-log-added":
|
||||
case "thread-stream/item-added":
|
||||
case "thread-stream/system-item-added":
|
||||
return patchObject(state, appendThreadStreamItemPatch(state, action.item));
|
||||
case "thread-stream/deduped-log-added":
|
||||
if (state.reportedLogs.has(action.text)) return state;
|
||||
return patchObject(state, {
|
||||
reportedLogs: new Set([...state.reportedLogs, action.text]),
|
||||
...appendMessageStreamItemPatch(state, action.item),
|
||||
...appendThreadStreamItemPatch(state, action.item),
|
||||
});
|
||||
case "message-stream/items-replaced":
|
||||
return messageStreamWithItems(state, action.items, {
|
||||
case "thread-stream/items-replaced":
|
||||
return threadStreamWithItems(state, action.items, {
|
||||
...definedPatch("historyCursor", action.historyCursor),
|
||||
...definedPatch("loadingHistory", action.loadingHistory),
|
||||
});
|
||||
case "message-stream/history-loading-set":
|
||||
case "thread-stream/history-loading-set":
|
||||
return patchObject(state, { loadingHistory: action.loading });
|
||||
case "message-stream/item-upserted":
|
||||
return upsertMessageStreamItem(state, action.item);
|
||||
case "message-stream/reasoning-completed":
|
||||
return completeReasoningInMessageStream(state, action.turnId);
|
||||
case "message-stream/assistant-delta-appended":
|
||||
return appendAssistantDeltaToMessageStream(state, action.itemId, action.turnId, action.delta, action.completeReasoning ?? false);
|
||||
case "message-stream/plan-delta-appended":
|
||||
return appendPlanDeltaToMessageStream(state, action.itemId, action.turnId, action.delta);
|
||||
case "message-stream/item-text-appended":
|
||||
return appendItemTextToMessageStream(state, action.itemId, action.turnId, action.label, action.delta, action.kind);
|
||||
case "message-stream/tool-output-appended":
|
||||
return appendToolOutputToMessageStream(state, action.itemId, action.turnId, action.delta, action.fallbackLabel);
|
||||
case "message-stream/item-output-appended":
|
||||
return appendItemOutputToMessageStream(state, action.itemId, action.turnId, action.delta, action.kind, action.fallbackText);
|
||||
case "message-stream/turn-diff-updated":
|
||||
case "thread-stream/item-upserted":
|
||||
return upsertThreadStreamItem(state, action.item);
|
||||
case "thread-stream/reasoning-completed":
|
||||
return completeReasoningInThreadStream(state, action.turnId);
|
||||
case "thread-stream/assistant-delta-appended":
|
||||
return appendAssistantDeltaToThreadStream(state, action.itemId, action.turnId, action.delta, action.completeReasoning ?? false);
|
||||
case "thread-stream/plan-delta-appended":
|
||||
return appendPlanDeltaToThreadStream(state, action.itemId, action.turnId, action.delta);
|
||||
case "thread-stream/item-text-appended":
|
||||
return appendItemTextToThreadStream(state, action.itemId, action.turnId, action.label, action.delta, action.kind);
|
||||
case "thread-stream/tool-output-appended":
|
||||
return appendToolOutputToThreadStream(state, action.itemId, action.turnId, action.delta, action.fallbackLabel);
|
||||
case "thread-stream/item-output-appended":
|
||||
return appendItemOutputToThreadStream(state, action.itemId, action.turnId, action.delta, action.kind, action.fallbackText);
|
||||
case "thread-stream/turn-diff-updated":
|
||||
return patchObject(state, {
|
||||
turnDiffs: updatedTurnDiffs(state.turnDiffs, action.turnId, action.diff),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessageStreamItemPatch(state: ChatMessageStreamState, item: MessageStreamItem): Partial<ChatMessageStreamState> {
|
||||
function appendThreadStreamItemPatch(state: ChatThreadStreamState, item: ThreadStreamItem): Partial<ChatThreadStreamState> {
|
||||
if (shouldUseActiveSegment(state.activeSegment, item)) {
|
||||
return { activeSegment: appendActiveSegmentItem(state.activeSegment, item) };
|
||||
}
|
||||
return { stableItems: [...state.stableItems, item] };
|
||||
}
|
||||
|
||||
function upsertMessageStreamItem(state: ChatMessageStreamState, item: MessageStreamItem): ChatMessageStreamState {
|
||||
function upsertThreadStreamItem(state: ChatThreadStreamState, item: ThreadStreamItem): ChatThreadStreamState {
|
||||
if (shouldUseActiveSegment(state.activeSegment, item)) {
|
||||
return patchObject(state, { activeSegment: upsertActiveSegmentItem(state.activeSegment, item) });
|
||||
}
|
||||
return patchObject(state, { stableItems: upsertMessageStreamItemById(state.stableItems, item) });
|
||||
return patchObject(state, { stableItems: upsertThreadStreamItemById(state.stableItems, item) });
|
||||
}
|
||||
|
||||
function appendAssistantDeltaToMessageStream(
|
||||
state: ChatMessageStreamState,
|
||||
function appendAssistantDeltaToThreadStream(
|
||||
state: ChatThreadStreamState,
|
||||
sourceItemId: string,
|
||||
turnId: string,
|
||||
delta: string,
|
||||
completeReasoning: boolean,
|
||||
): ChatMessageStreamState {
|
||||
const current = completeReasoning ? completeReasoningInMessageStream(state, turnId) : state;
|
||||
): ChatThreadStreamState {
|
||||
const current = completeReasoning ? completeReasoningInThreadStream(state, turnId) : state;
|
||||
return updateActiveSegment(current, turnId, (segment) => {
|
||||
const index = segment.indexBySourceItemId.get(sourceItemId);
|
||||
if (index !== undefined) {
|
||||
return replaceActiveSegmentItem(segment, index, (item) =>
|
||||
item.kind === "message" && item.messageKind === "assistantResponse"
|
||||
item.kind === "dialogue" && item.dialogueKind === "assistantResponse"
|
||||
? {
|
||||
...item,
|
||||
text: `${item.text}${delta}`,
|
||||
copyText: `${item.text}${delta}`,
|
||||
turnId: item.turnId ?? turnId,
|
||||
messageState: "streaming",
|
||||
dialogueState: "streaming",
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
return appendActiveSegmentItem(segment, {
|
||||
id: sourceItemId,
|
||||
kind: "message",
|
||||
messageKind: "assistantResponse",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "assistantResponse",
|
||||
role: "assistant",
|
||||
text: delta,
|
||||
copyText: delta,
|
||||
turnId,
|
||||
sourceItemId,
|
||||
provenance: { source: "appServer", channel: "notification", event: "streamingDelta", sourceItemId },
|
||||
messageState: "streaming",
|
||||
dialogueState: "streaming",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function appendPlanDeltaToMessageStream(
|
||||
state: ChatMessageStreamState,
|
||||
function appendPlanDeltaToThreadStream(
|
||||
state: ChatThreadStreamState,
|
||||
sourceItemId: string,
|
||||
turnId: string,
|
||||
delta: string,
|
||||
): ChatMessageStreamState {
|
||||
): ChatThreadStreamState {
|
||||
return updateActiveSegment(state, turnId, (segment) => {
|
||||
const index = segment.indexBySourceItemId.get(sourceItemId);
|
||||
if (index !== undefined) {
|
||||
return replaceActiveSegmentItem(segment, index, (item) => {
|
||||
if (item.kind !== "message" || item.role !== "assistant") return item;
|
||||
if (item.kind !== "dialogue" || item.role !== "assistant") return item;
|
||||
const text = normalizeProposedPlanMarkdown(`${item.text}${delta}`);
|
||||
return {
|
||||
...item,
|
||||
messageKind: "proposedPlan",
|
||||
dialogueKind: "proposedPlan",
|
||||
text,
|
||||
copyText: text,
|
||||
turnId: item.turnId ?? turnId,
|
||||
messageState: "streaming",
|
||||
dialogueState: "streaming",
|
||||
};
|
||||
});
|
||||
}
|
||||
const text = normalizeProposedPlanMarkdown(delta);
|
||||
return appendActiveSegmentItem(segment, {
|
||||
id: sourceItemId,
|
||||
kind: "message",
|
||||
messageKind: "proposedPlan",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "proposedPlan",
|
||||
role: "assistant",
|
||||
text,
|
||||
copyText: text,
|
||||
turnId,
|
||||
sourceItemId,
|
||||
provenance: { source: "appServer", channel: "notification", event: "streamingDelta", sourceItemId },
|
||||
messageState: "streaming",
|
||||
dialogueState: "streaming",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function appendItemTextToMessageStream(
|
||||
state: ChatMessageStreamState,
|
||||
function appendItemTextToThreadStream(
|
||||
state: ChatThreadStreamState,
|
||||
sourceItemId: string,
|
||||
turnId: string,
|
||||
label: string,
|
||||
delta: string,
|
||||
kind: "tool" | "hook" | "reasoning",
|
||||
): ChatMessageStreamState {
|
||||
): ChatThreadStreamState {
|
||||
return updateActiveSegment(state, turnId, (segment) => {
|
||||
const index = segment.indexBySourceItemId.get(sourceItemId);
|
||||
if (index !== undefined) {
|
||||
return replaceActiveSegmentItem(segment, index, (item) => appendTextToMatchingStreamItemKind(item, kind, delta));
|
||||
}
|
||||
return appendActiveSegmentItem(segment, {
|
||||
...streamedTextMessageStreamItem({
|
||||
...streamedTextThreadStreamItem({
|
||||
id: sourceItemId,
|
||||
kind,
|
||||
label,
|
||||
|
|
@ -337,13 +337,13 @@ function appendItemTextToMessageStream(
|
|||
});
|
||||
}
|
||||
|
||||
function appendToolOutputToMessageStream(
|
||||
state: ChatMessageStreamState,
|
||||
function appendToolOutputToThreadStream(
|
||||
state: ChatThreadStreamState,
|
||||
sourceItemId: string,
|
||||
turnId: string,
|
||||
delta: string,
|
||||
fallbackLabel: string,
|
||||
): ChatMessageStreamState {
|
||||
): ChatThreadStreamState {
|
||||
return updateActiveSegment(state, turnId, (segment) => {
|
||||
const index = segment.indexBySourceItemId.get(sourceItemId);
|
||||
if (index !== undefined) {
|
||||
|
|
@ -355,7 +355,7 @@ function appendToolOutputToMessageStream(
|
|||
}
|
||||
return appendActiveSegmentItem(
|
||||
segment,
|
||||
streamedToolOutputMessageStreamItem({
|
||||
streamedToolOutputThreadStreamItem({
|
||||
id: sourceItemId,
|
||||
turnId,
|
||||
output: delta,
|
||||
|
|
@ -365,14 +365,14 @@ function appendToolOutputToMessageStream(
|
|||
});
|
||||
}
|
||||
|
||||
function appendItemOutputToMessageStream(
|
||||
state: ChatMessageStreamState,
|
||||
function appendItemOutputToThreadStream(
|
||||
state: ChatThreadStreamState,
|
||||
sourceItemId: string,
|
||||
turnId: string,
|
||||
delta: string,
|
||||
kind: "command" | "fileChange",
|
||||
fallbackText: string,
|
||||
): ChatMessageStreamState {
|
||||
): ChatThreadStreamState {
|
||||
return updateActiveSegment(state, turnId, (segment) => {
|
||||
const index = segment.indexBySourceItemId.get(sourceItemId);
|
||||
if (index !== undefined) {
|
||||
|
|
@ -382,7 +382,7 @@ function appendItemOutputToMessageStream(
|
|||
}
|
||||
return appendActiveSegmentItem(
|
||||
segment,
|
||||
streamedItemOutputMessageStreamItem({
|
||||
streamedItemOutputThreadStreamItem({
|
||||
id: sourceItemId,
|
||||
kind,
|
||||
turnId,
|
||||
|
|
@ -393,7 +393,7 @@ function appendItemOutputToMessageStream(
|
|||
});
|
||||
}
|
||||
|
||||
function completeReasoningInMessageStream(state: ChatMessageStreamState, turnId: string): ChatMessageStreamState {
|
||||
function completeReasoningInThreadStream(state: ChatThreadStreamState, turnId: string): ChatThreadStreamState {
|
||||
const stableItems = completeReasoningItems(state.stableItems, turnId);
|
||||
const activeSegment = state.activeSegment;
|
||||
|
||||
|
|
@ -412,10 +412,10 @@ function completeReasoningInMessageStream(state: ChatMessageStreamState, turnId:
|
|||
}
|
||||
|
||||
function updateActiveSegment(
|
||||
state: ChatMessageStreamState,
|
||||
state: ChatThreadStreamState,
|
||||
turnId: string,
|
||||
update: (segment: ChatMessageStreamActiveSegment) => ChatMessageStreamActiveSegment,
|
||||
): ChatMessageStreamState {
|
||||
update: (segment: ChatThreadStreamActiveSegment) => ChatThreadStreamActiveSegment,
|
||||
): ChatThreadStreamState {
|
||||
const activeSegment = state.activeSegment;
|
||||
if (activeSegment?.turnId && activeSegment.turnId !== turnId) return state;
|
||||
const segment =
|
||||
|
|
@ -427,38 +427,34 @@ function updateActiveSegment(
|
|||
return patchObject(state, { activeSegment: update(segment) });
|
||||
}
|
||||
|
||||
function appendTextToMatchingStreamItemKind(
|
||||
item: MessageStreamItem,
|
||||
kind: "tool" | "hook" | "reasoning",
|
||||
delta: string,
|
||||
): MessageStreamItem {
|
||||
function appendTextToMatchingStreamItemKind(item: ThreadStreamItem, kind: "tool" | "hook" | "reasoning", delta: string): ThreadStreamItem {
|
||||
if (item.kind !== kind) return item;
|
||||
return { ...item, text: `${item.text ?? ""}${delta}` };
|
||||
}
|
||||
|
||||
function shouldUseActiveSegment(
|
||||
segment: ChatMessageStreamActiveSegment | null,
|
||||
item: MessageStreamItem,
|
||||
): segment is ChatMessageStreamActiveSegment {
|
||||
segment: ChatThreadStreamActiveSegment | null,
|
||||
item: ThreadStreamItem,
|
||||
): segment is ChatThreadStreamActiveSegment {
|
||||
if (!segment) return false;
|
||||
return !item.turnId || !segment.turnId || item.turnId === segment.turnId;
|
||||
}
|
||||
|
||||
function appendActiveSegmentItem(segment: ChatMessageStreamActiveSegment, item: MessageStreamItem): ChatMessageStreamActiveSegment {
|
||||
function appendActiveSegmentItem(segment: ChatThreadStreamActiveSegment, item: ThreadStreamItem): ChatThreadStreamActiveSegment {
|
||||
return activeSegmentFromItems(segment.turnId, [...segment.items, item]);
|
||||
}
|
||||
|
||||
function upsertActiveSegmentItem(segment: ChatMessageStreamActiveSegment, item: MessageStreamItem): ChatMessageStreamActiveSegment {
|
||||
function upsertActiveSegmentItem(segment: ChatThreadStreamActiveSegment, item: ThreadStreamItem): ChatThreadStreamActiveSegment {
|
||||
const index = segment.indexById.get(item.id);
|
||||
if (index === undefined) return appendActiveSegmentItem(segment, item);
|
||||
return replaceActiveSegmentItem(segment, index, (previous) => upsertMessageStreamItemById([previous], item)[0] ?? item);
|
||||
return replaceActiveSegmentItem(segment, index, (previous) => upsertThreadStreamItemById([previous], item)[0] ?? item);
|
||||
}
|
||||
|
||||
function replaceActiveSegmentItem(
|
||||
segment: ChatMessageStreamActiveSegment,
|
||||
segment: ChatThreadStreamActiveSegment,
|
||||
index: number,
|
||||
replacement: (item: MessageStreamItem) => MessageStreamItem,
|
||||
): ChatMessageStreamActiveSegment {
|
||||
replacement: (item: ThreadStreamItem) => ThreadStreamItem,
|
||||
): ChatThreadStreamActiveSegment {
|
||||
const previous = segment.items[index];
|
||||
if (!previous) return segment;
|
||||
const next = replacement(previous);
|
||||
|
|
@ -468,7 +464,7 @@ function replaceActiveSegmentItem(
|
|||
return activeSegmentFromItems(segment.turnId, items);
|
||||
}
|
||||
|
||||
function activeSegmentFromItems(turnId: string | null, items: readonly MessageStreamItem[]): ChatMessageStreamActiveSegment {
|
||||
function activeSegmentFromItems(turnId: string | null, items: readonly ThreadStreamItem[]): ChatThreadStreamActiveSegment {
|
||||
const indexById = new Map<string, number>();
|
||||
const indexBySourceItemId = new Map<string, number>();
|
||||
items.forEach((item, index) => {
|
||||
|
|
@ -488,7 +484,7 @@ function updatedTurnDiffs(turnDiffs: ReadonlyMap<string, string>, turnId: string
|
|||
return next;
|
||||
}
|
||||
|
||||
function orderedTurnIds(items: readonly MessageStreamItem[]): string[] {
|
||||
function orderedTurnIds(items: readonly ThreadStreamItem[]): string[] {
|
||||
const turnIds: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of items) {
|
||||
|
|
@ -499,17 +495,17 @@ function orderedTurnIds(items: readonly MessageStreamItem[]): string[] {
|
|||
return turnIds;
|
||||
}
|
||||
|
||||
function latestTurnId(items: readonly MessageStreamItem[]): string | null {
|
||||
function latestTurnId(items: readonly ThreadStreamItem[]): string | null {
|
||||
for (const item of [...items].reverse()) {
|
||||
if (item.turnId) return item.turnId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function promptMessageForTurn(items: readonly MessageStreamItem[], turnId: string): MessageStreamMessageItem | null {
|
||||
const classification = messageStreamSemanticClassifications(items).find(
|
||||
(classification) => classification.item.turnId === turnId && messageStreamIsTurnInitiator(classification),
|
||||
function promptMessageForTurn(items: readonly ThreadStreamItem[], turnId: string): ThreadStreamDialogueItem | null {
|
||||
const classification = threadStreamSemanticClassifications(items).find(
|
||||
(classification) => classification.item.turnId === turnId && threadStreamIsTurnInitiator(classification),
|
||||
);
|
||||
const item = classification?.item;
|
||||
return item?.kind === "message" ? item : null;
|
||||
return item?.kind === "dialogue" ? item : null;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ThreadGoal, ThreadGoalStatus, ThreadGoalUpdate } from "../../../../domain/threads/goal";
|
||||
import { goalChangeItem } from "../../domain/message-stream/factories/goal-items";
|
||||
import type { GoalMessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { goalChangeItem } from "../../domain/thread-stream/factories/goal-items";
|
||||
import type { GoalThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import type { LocalIdSource } from "../local-id-source";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import type { ThreadGoalReadTransport, ThreadGoalTransport } from "./goal-transport";
|
||||
|
|
@ -10,7 +10,7 @@ export interface ThreadGoalSyncHost {
|
|||
goalTransport: ThreadGoalReadTransport;
|
||||
localItemIds: LocalIdSource;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addGoalEvent: (item: GoalMessageStreamItem) => void;
|
||||
addGoalEvent: (item: GoalThreadStreamItem) => void;
|
||||
refreshLiveState: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { messageStreamItems } from "../state/message-stream";
|
||||
import type { ChatAction, ChatState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { threadStreamItems } from "../state/thread-stream";
|
||||
import type { ThreadHistoryPage, ThreadHistoryTransport } from "./thread-loading-transport";
|
||||
|
||||
export interface HistoryControllerHost {
|
||||
|
|
@ -33,7 +33,7 @@ export class HistoryController {
|
|||
|
||||
invalidate(): void {
|
||||
this.lifecycle = transitionThreadHistoryLoadLifecycle(this.lifecycle, { type: "invalidated" });
|
||||
this.dispatch({ type: "message-stream/history-loading-set", loading: false });
|
||||
this.dispatch({ type: "thread-stream/history-loading-set", loading: false });
|
||||
}
|
||||
|
||||
async loadLatest(threadId = this.state.activeThread.id): Promise<void> {
|
||||
|
|
@ -57,7 +57,7 @@ export class HistoryController {
|
|||
this.host.setThreadTurnPresence(response.hadTurns);
|
||||
this.host.showLatestPageAtBottom();
|
||||
this.dispatch({
|
||||
type: "message-stream/items-replaced",
|
||||
type: "thread-stream/items-replaced",
|
||||
items: response.items,
|
||||
historyCursor: response.nextCursor,
|
||||
});
|
||||
|
|
@ -66,20 +66,20 @@ export class HistoryController {
|
|||
|
||||
async loadOlder(): Promise<void> {
|
||||
const state = this.state;
|
||||
if (!state.activeThread.id || !state.messageStream.historyCursor || state.messageStream.loadingHistory) return;
|
||||
if (!state.activeThread.id || !state.threadStream.historyCursor || state.threadStream.loadingHistory) return;
|
||||
const threadId = state.activeThread.id;
|
||||
const cursor = state.messageStream.historyCursor;
|
||||
const cursor = state.threadStream.historyCursor;
|
||||
const load = this.startLoading(threadId, "older");
|
||||
try {
|
||||
const response = await this.host.historyTransport.readHistoryPage(threadId, cursor, 20);
|
||||
if (!response) return;
|
||||
if (this.isStale(load)) return;
|
||||
const current = this.state;
|
||||
const currentItems = messageStreamItems(current.messageStream);
|
||||
const currentItems = threadStreamItems(current.threadStream);
|
||||
const olderItems = response.items;
|
||||
const existingIds = new Set(currentItems.map((item) => item.id));
|
||||
this.dispatch({
|
||||
type: "message-stream/items-replaced",
|
||||
type: "thread-stream/items-replaced",
|
||||
items: [...olderItems.filter((item) => !existingIds.has(item.id)), ...currentItems],
|
||||
historyCursor: response.nextCursor,
|
||||
});
|
||||
|
|
@ -94,14 +94,14 @@ export class HistoryController {
|
|||
private startLoading(threadId: string, mode: ActiveThreadHistoryLoad["mode"]): ActiveThreadHistoryLoad {
|
||||
const load: ActiveThreadHistoryLoad = { kind: "loading", threadId, mode };
|
||||
this.lifecycle = transitionThreadHistoryLoadLifecycle(this.lifecycle, { type: "started", load });
|
||||
this.dispatch({ type: "message-stream/history-loading-set", loading: true });
|
||||
this.dispatch({ type: "thread-stream/history-loading-set", loading: true });
|
||||
return load;
|
||||
}
|
||||
|
||||
private finishLoading(load: ActiveThreadHistoryLoad): void {
|
||||
if (this.isStale(load)) return;
|
||||
this.lifecycle = transitionThreadHistoryLoadLifecycle(this.lifecycle, { type: "finished", load });
|
||||
this.dispatch({ type: "message-stream/history-loading-set", loading: false });
|
||||
this.dispatch({ type: "thread-stream/history-loading-set", loading: false });
|
||||
}
|
||||
|
||||
private isStale(load: ActiveThreadHistoryLoad): boolean {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { threadRenameDraftTitle } from "../../../../domain/threads/title";
|
||||
import type { ThreadTitleContext } from "../../../../domain/threads/title-generation-model";
|
||||
import { messageStreamItems } from "../state/message-stream";
|
||||
import type { ChatAction, ChatState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { threadStreamItems } from "../state/thread-stream";
|
||||
import { type ChatRenameGeneratingUiState, type ChatRenameUiState, renameGenerationStillActive } from "../state/ui-state";
|
||||
import { firstThreadTitleContextFromMessageStreamItems } from "./title-context";
|
||||
import { firstThreadTitleContextFromThreadStreamItems } from "./title-context";
|
||||
|
||||
interface RenameEditState {
|
||||
draft: string;
|
||||
|
|
@ -113,7 +113,7 @@ export function createThreadRenameEditorActions(host: ThreadRenameEditorActionsH
|
|||
}
|
||||
|
||||
export function activeThreadRenameTitleContext(state: ChatState, threadId: string): ThreadTitleContext | null {
|
||||
return state.activeThread.id === threadId ? firstThreadTitleContextFromMessageStreamItems(messageStreamItems(state.messageStream)) : null;
|
||||
return state.activeThread.id === threadId ? firstThreadTitleContextFromThreadStreamItems(threadStreamItems(state.threadStream)) : null;
|
||||
}
|
||||
|
||||
function renameState(host: ThreadRenameEditorActionsHost): ChatRenameUiState {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ThreadTokenUsage } from "../../../../domain/runtime/metrics";
|
||||
import { resumedThreadAction } from "../state/actions";
|
||||
import { messageStreamIsEmpty } from "../state/message-stream";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { threadStreamIsEmpty } from "../state/thread-stream";
|
||||
import type { HistoryController } from "./history-controller";
|
||||
import type { RestorationController } from "./restoration-controller";
|
||||
import type { ActiveChatResume, ChatResumeWorkTracker } from "./resume-work";
|
||||
|
|
@ -55,7 +55,7 @@ async function resumeThread(host: ResumeActionsHost, threadId: string): Promise<
|
|||
if (isStaleResume(host, resume)) return;
|
||||
await host.syncThreadGoal(response.activation.thread.id);
|
||||
if (isStaleResume(host, resume)) return;
|
||||
const renderFallbackMessage = messageStreamIsEmpty(host.stateStore.getState().messageStream);
|
||||
const renderFallbackMessage = threadStreamIsEmpty(host.stateStore.getState().threadStream);
|
||||
if (renderFallbackMessage) {
|
||||
host.addSystemMessage(`Resumed thread ${response.activation.thread.id}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ThreadActivationSnapshot } from "../../../../domain/threads/activation";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
|
||||
export interface ThreadHistoryPage {
|
||||
items: MessageStreamItem[];
|
||||
items: ThreadStreamItem[];
|
||||
nextCursor: string | null;
|
||||
hadTurns: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { inheritedForkThreadName, type Thread } from "../../../../domain/threads/model";
|
||||
import { activeThreadRuntimeState } from "../../domain/runtime/state";
|
||||
import { chatTurnBusy } from "../conversation/turn-state";
|
||||
import { resumedThreadActionFromActiveRuntime } from "../state/actions";
|
||||
import { messageStreamRollbackCandidate, messageStreamTurnsAfterTurnId } from "../state/message-stream";
|
||||
import type { ChatAction, ChatState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { threadStreamRollbackCandidate, threadStreamTurnsAfterTurnId } from "../state/thread-stream";
|
||||
import { chatTurnBusy } from "../turns/turn-state";
|
||||
import type { ThreadMutationTransport } from "./thread-mutation-transport";
|
||||
|
||||
const STATUS_COMPACTION_REQUESTED = "Compaction requested.";
|
||||
|
|
@ -113,7 +113,7 @@ async function forkThreadFromTurn(
|
|||
}
|
||||
const scope = captureThreadManagementPanelScope(host, threadId);
|
||||
|
||||
const selectedTurnDistanceFromEnd = turnId ? messageStreamTurnsAfterTurnId(threadManagementState(host).messageStream, turnId) : 0;
|
||||
const selectedTurnDistanceFromEnd = turnId ? threadStreamTurnsAfterTurnId(threadManagementState(host).threadStream, turnId) : 0;
|
||||
if (selectedTurnDistanceFromEnd === null) {
|
||||
host.addSystemMessage("Could not find the selected turn to fork.");
|
||||
return;
|
||||
|
|
@ -173,7 +173,7 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
|
|||
}
|
||||
const scope = captureThreadManagementPanelScope(host, threadId);
|
||||
|
||||
const candidate = messageStreamRollbackCandidate(threadManagementState(host).messageStream);
|
||||
const candidate = threadStreamRollbackCandidate(threadManagementState(host).threadStream);
|
||||
if (!candidate) {
|
||||
host.addSystemMessage("No completed turn to roll back.");
|
||||
return;
|
||||
|
|
@ -194,7 +194,7 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
|
|||
}),
|
||||
);
|
||||
threadManagementDispatch(host, {
|
||||
type: "message-stream/items-replaced",
|
||||
type: "thread-stream/items-replaced",
|
||||
items: snapshot.items,
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
|
||||
export interface ThreadRollbackSnapshot {
|
||||
thread: Thread;
|
||||
cwd: string;
|
||||
items: MessageStreamItem[];
|
||||
items: ThreadStreamItem[];
|
||||
}
|
||||
|
||||
export interface ThreadMutationTransport {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { chatTurnBusy } from "../conversation/turn-state";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { chatTurnBusy } from "../turns/turn-state";
|
||||
import type { ActiveThreadIdentitySync } from "./active-thread-identity-sync";
|
||||
import { canSwitchToThread } from "./thread-switching";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { chatTurnBusy } from "../conversation/turn-state";
|
||||
import type { ChatState } from "../state/root-reducer";
|
||||
import { chatTurnBusy } from "../turns/turn-state";
|
||||
|
||||
export function canSwitchToThread(state: ChatState, threadId: string): boolean {
|
||||
return !chatTurnBusy(state) || threadId === state.activeThread.id;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { type ThreadTitleContext, threadTitleContextPromptText } from "../../../../domain/threads/title-generation-model";
|
||||
import type { MessageStreamItem, MessageStreamMessageItem } from "../../domain/message-stream/items";
|
||||
import { isCompletedTurnOutcomeMessage } from "../../domain/message-stream/selectors";
|
||||
import type { ThreadStreamDialogueItem, ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { isCompletedTurnOutcomeMessage } from "../../domain/thread-stream/selectors";
|
||||
|
||||
export function threadTitleContextFromMessageStreamItems(turnId: string, items: readonly MessageStreamItem[]): ThreadTitleContext | null {
|
||||
export function threadTitleContextFromThreadStreamItems(turnId: string, items: readonly ThreadStreamItem[]): ThreadTitleContext | null {
|
||||
const turnItems = items.filter((item) => item.turnId === turnId);
|
||||
const userRequest = turnItems.find(isUserMessageStreamItem)?.text.trim() ?? precedingUnscopedTitleSeed(turnId, items) ?? "";
|
||||
const userRequest = turnItems.find(isUserThreadStreamDialogueItem)?.text.trim() ?? precedingUnscopedTitleSeed(turnId, items) ?? "";
|
||||
const assistantResponse = [...turnItems].reverse().find(isCompletedTurnOutcomeMessageItem)?.text.trim() ?? "";
|
||||
if (!userRequest || !assistantResponse) return null;
|
||||
return {
|
||||
|
|
@ -13,32 +13,32 @@ export function threadTitleContextFromMessageStreamItems(turnId: string, items:
|
|||
};
|
||||
}
|
||||
|
||||
export function firstThreadTitleContextFromMessageStreamItems(items: readonly MessageStreamItem[]): ThreadTitleContext | null {
|
||||
export function firstThreadTitleContextFromThreadStreamItems(items: readonly ThreadStreamItem[]): ThreadTitleContext | null {
|
||||
const turnIds = new Set<string>();
|
||||
for (const item of items) {
|
||||
if (!item.turnId || turnIds.has(item.turnId)) continue;
|
||||
turnIds.add(item.turnId);
|
||||
const context = threadTitleContextFromMessageStreamItems(item.turnId, items);
|
||||
const context = threadTitleContextFromThreadStreamItems(item.turnId, items);
|
||||
if (context) return context;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isUserMessageStreamItem(item: MessageStreamItem): item is MessageStreamMessageItem & { role: "user" } {
|
||||
return item.kind === "message" && item.role === "user";
|
||||
function isUserThreadStreamDialogueItem(item: ThreadStreamItem): item is ThreadStreamDialogueItem & { role: "user" } {
|
||||
return item.kind === "dialogue" && item.role === "user";
|
||||
}
|
||||
|
||||
function isCompletedTurnOutcomeMessageItem(item: MessageStreamItem): item is MessageStreamMessageItem {
|
||||
return item.kind === "message" && isCompletedTurnOutcomeMessage(item);
|
||||
function isCompletedTurnOutcomeMessageItem(item: ThreadStreamItem): item is ThreadStreamDialogueItem {
|
||||
return item.kind === "dialogue" && isCompletedTurnOutcomeMessage(item);
|
||||
}
|
||||
|
||||
function precedingUnscopedTitleSeed(turnId: string, items: readonly MessageStreamItem[]): string | null {
|
||||
function precedingUnscopedTitleSeed(turnId: string, items: readonly ThreadStreamItem[]): string | null {
|
||||
const firstTurnItemIndex = items.findIndex((item) => item.turnId === turnId);
|
||||
if (firstTurnItemIndex < 1) return null;
|
||||
for (let index = firstTurnItemIndex - 1; index >= 0; index -= 1) {
|
||||
const item = items[index];
|
||||
if (!item || item.turnId) return null;
|
||||
if (item.kind === "message" && item.role === "user") return item.text.trim();
|
||||
if (item.kind === "dialogue" && item.role === "user") return item.text.trim();
|
||||
if (item.kind === "goal" && item.objective) return item.objective.trim();
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { CodexInput } from "../../../../domain/chat/input";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { MessageStreamNoticeSection } from "../../domain/message-stream/items";
|
||||
import type { ThreadStreamNoticeSection } from "../../domain/thread-stream/items";
|
||||
import type { ComposerInputSnapshot } from "../composer/input-snapshot";
|
||||
import type { LocalIdSource } from "../local-id-source";
|
||||
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
|
||||
|
|
@ -14,7 +14,7 @@ import { executeSlashCommandWithState, type SlashCommandExecutorHost } from "./s
|
|||
import { createTurnSubmissionActions } from "./turn-submission-actions";
|
||||
import type { ChatTurnTransport } from "./turn-transport";
|
||||
|
||||
export interface ConversationTurnActionsContext {
|
||||
export interface TurnWorkflowContext {
|
||||
stateStore: ChatStateStore;
|
||||
localItemIds: LocalIdSource;
|
||||
connectionAvailable: () => boolean;
|
||||
|
|
@ -24,15 +24,15 @@ export interface ConversationTurnActionsContext {
|
|||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: ThreadStreamNoticeSection[]) => void;
|
||||
};
|
||||
runtime: {
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
permissionDetails: () => MessageStreamNoticeSection[];
|
||||
connectionDiagnosticDetails: () => ThreadStreamNoticeSection[];
|
||||
permissionDetails: () => ThreadStreamNoticeSection[];
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
statusSummaryLines: () => string[];
|
||||
toolInventoryDetails: () => MessageStreamNoticeSection[] | Promise<MessageStreamNoticeSection[]>;
|
||||
toolInventoryDetails: () => ThreadStreamNoticeSection[] | Promise<ThreadStreamNoticeSection[]>;
|
||||
};
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: () => Promise<boolean>;
|
||||
|
|
@ -52,15 +52,15 @@ export interface ConversationTurnActionsContext {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ConversationTurnActionsRefs {
|
||||
threadStarter: ConversationThreadStarter;
|
||||
export interface TurnWorkflowRefs {
|
||||
threadStarter: TurnWorkflowThreadStarter;
|
||||
runtimeSettings: ChatRuntimeSettingsActions;
|
||||
threadActions: ThreadManagementActions;
|
||||
reconnectPanel: () => Promise<void>;
|
||||
goals: GoalActions;
|
||||
}
|
||||
|
||||
interface ConversationThreadStarter {
|
||||
interface TurnWorkflowThreadStarter {
|
||||
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<{ threadId: string } | null>;
|
||||
}
|
||||
|
||||
|
|
@ -68,15 +68,12 @@ interface PlanImplementation {
|
|||
implement: (itemId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ConversationTurnActions {
|
||||
export interface TurnWorkflowActions {
|
||||
planImplementation: PlanImplementation;
|
||||
composerSubmit: ComposerSubmitActions;
|
||||
}
|
||||
|
||||
export function createConversationTurnActions(
|
||||
context: ConversationTurnActionsContext,
|
||||
refs: ConversationTurnActionsRefs,
|
||||
): ConversationTurnActions {
|
||||
export function createTurnWorkflowActions(context: TurnWorkflowContext, refs: TurnWorkflowRefs): TurnWorkflowActions {
|
||||
const { stateStore, localItemIds, connectionAvailable, turnTransport, referThread, clipUrl, status, runtime, thread, composer, scroll } =
|
||||
context;
|
||||
const turnSubmission = createTurnSubmissionActions({
|
||||
|
|
@ -159,7 +156,7 @@ export function createConversationTurnActions(
|
|||
};
|
||||
}
|
||||
|
||||
async function startThreadForGoal(starter: ConversationThreadStarter, objective: string): Promise<string | null> {
|
||||
async function startThreadForGoal(starter: TurnWorkflowThreadStarter, objective: string): Promise<string | null> {
|
||||
const response = await starter.startThread(objective, { syncGoal: false });
|
||||
return response?.threadId ?? null;
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import type { CodexInput } from "../../../../domain/chat/input";
|
||||
import { fileMentionsFromInput } from "../../domain/message-stream/format/file-mentions";
|
||||
import { userMessageDisplayText } from "../../domain/message-stream/format/user-message-text";
|
||||
import type { MessageStreamFileMention, MessageStreamItem, MessageStreamMessageItem } from "../../domain/message-stream/items";
|
||||
import { isLocalSteerMessageClientId } from "../../domain/message-stream/local-message-ids";
|
||||
import type { MessageStreamItemProvenance } from "../../domain/message-stream/provenance";
|
||||
import { attachHookRunsToTurn } from "../../domain/message-stream/updates";
|
||||
import { fileMentionsFromInput } from "../../domain/thread-stream/format/file-mentions";
|
||||
import { userMessageDisplayText } from "../../domain/thread-stream/format/user-message-text";
|
||||
import type { ThreadStreamDialogueItem, ThreadStreamFileMention, ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { isLocalSteerMessageClientId } from "../../domain/thread-stream/local-message-ids";
|
||||
import type { ThreadStreamItemProvenance } from "../../domain/thread-stream/provenance";
|
||||
import { attachHookRunsToTurn } from "../../domain/thread-stream/updates";
|
||||
import type { PendingTurnStart } from "./turn-state";
|
||||
|
||||
interface LocalUserMessageParams {
|
||||
|
|
@ -12,12 +12,12 @@ interface LocalUserMessageParams {
|
|||
text: string;
|
||||
copyText?: string;
|
||||
turnId?: string;
|
||||
referencedThread?: MessageStreamMessageItem["referencedThread"];
|
||||
mentionedFiles?: readonly MessageStreamFileMention[];
|
||||
referencedThread?: ThreadStreamDialogueItem["referencedThread"];
|
||||
mentionedFiles?: readonly ThreadStreamFileMention[];
|
||||
}
|
||||
|
||||
export interface OptimisticTurnStartAckParams {
|
||||
items: readonly MessageStreamItem[];
|
||||
items: readonly ThreadStreamItem[];
|
||||
optimisticUserId: string;
|
||||
turnId: string;
|
||||
pendingTurnStart: PendingTurnStart | null;
|
||||
|
|
@ -28,7 +28,7 @@ export interface LocalUserMessageFromInputParams extends Omit<LocalUserMessagePa
|
|||
}
|
||||
|
||||
export interface OptimisticTurnStart {
|
||||
item: MessageStreamMessageItem;
|
||||
item: ThreadStreamDialogueItem;
|
||||
pendingTurnStart: PendingTurnStart;
|
||||
}
|
||||
|
||||
|
|
@ -42,17 +42,17 @@ export interface TurnStartAckMatchParams {
|
|||
}
|
||||
|
||||
export interface FailedTurnStartCleanupParams {
|
||||
items: readonly MessageStreamItem[];
|
||||
items: readonly ThreadStreamItem[];
|
||||
optimisticUserId: string | null;
|
||||
pendingTurnStart: PendingTurnStart | null;
|
||||
}
|
||||
|
||||
function localUserMessageItem(params: LocalUserMessageParams): MessageStreamMessageItem {
|
||||
function localUserMessageItem(params: LocalUserMessageParams): ThreadStreamDialogueItem {
|
||||
const mentionedFiles = params.mentionedFiles ?? [];
|
||||
return {
|
||||
id: params.id,
|
||||
kind: "message",
|
||||
messageKind: "user",
|
||||
kind: "dialogue",
|
||||
dialogueKind: "user",
|
||||
role: "user",
|
||||
text: params.text,
|
||||
copyText: params.copyText ?? params.text,
|
||||
|
|
@ -63,7 +63,7 @@ function localUserMessageItem(params: LocalUserMessageParams): MessageStreamMess
|
|||
};
|
||||
}
|
||||
|
||||
function localUserMessageProvenance(id: string): MessageStreamItemProvenance {
|
||||
function localUserMessageProvenance(id: string): ThreadStreamItemProvenance {
|
||||
return {
|
||||
source: "localUser",
|
||||
channel: "optimistic",
|
||||
|
|
@ -72,7 +72,7 @@ function localUserMessageProvenance(id: string): MessageStreamItemProvenance {
|
|||
};
|
||||
}
|
||||
|
||||
export function localUserMessageItemFromInput(params: LocalUserMessageFromInputParams): MessageStreamMessageItem {
|
||||
export function localUserMessageItemFromInput(params: LocalUserMessageFromInputParams): ThreadStreamDialogueItem {
|
||||
return localUserMessageItem({
|
||||
id: params.id,
|
||||
text: userMessageDisplayText(params.text, params.codexInput),
|
||||
|
|
@ -98,13 +98,13 @@ export function shouldAcknowledgeTurnStart(params: TurnStartAckMatchParams): boo
|
|||
);
|
||||
}
|
||||
|
||||
export function acknowledgeOptimisticTurnStart(params: OptimisticTurnStartAckParams): MessageStreamItem[] {
|
||||
export function acknowledgeOptimisticTurnStart(params: OptimisticTurnStartAckParams): ThreadStreamItem[] {
|
||||
const items = params.items.map((item) => (item.id === params.optimisticUserId ? { ...item, turnId: params.turnId } : item));
|
||||
if (!params.pendingTurnStart) return items;
|
||||
return attachHookRunsToTurn(items, params.turnId, params.pendingTurnStart.promptSubmitHookItemIds, params.pendingTurnStart.anchorItemId);
|
||||
}
|
||||
|
||||
export function cleanupFailedTurnStart(params: FailedTurnStartCleanupParams): MessageStreamItem[] {
|
||||
export function cleanupFailedTurnStart(params: FailedTurnStartCleanupParams): ThreadStreamItem[] {
|
||||
const withoutOptimisticUser = params.optimisticUserId
|
||||
? params.items.filter((item) => item.id !== params.optimisticUserId)
|
||||
: [...params.items];
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { latestImplementablePlanTargetFromItems, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import type { ChatRuntimeState } from "../../domain/runtime/state";
|
||||
import { type ChatMessageStreamState, messageStreamItems } from "../state/message-stream";
|
||||
import { latestImplementablePlanTargetFromItems, type PlanImplementationTarget } from "../../domain/thread-stream/selectors";
|
||||
import type { ChatActiveThreadState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { type ChatThreadStreamState, threadStreamItems } from "../state/thread-stream";
|
||||
import { type ChatTurnState, chatTurnBusy } from "./turn-state";
|
||||
|
||||
const IMPLEMENT_PLAN_PROMPT = "Please implement this plan.";
|
||||
|
|
@ -18,7 +18,7 @@ export function implementPlanTargetFromState(state: {
|
|||
activeThread: Pick<ChatActiveThreadState, "id">;
|
||||
turn: ChatTurnState;
|
||||
runtime: { pending: Pick<ChatRuntimeState["pending"], "collaborationMode"> };
|
||||
messageStream: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">;
|
||||
threadStream: Pick<ChatThreadStreamState, "stableItems" | "activeSegment">;
|
||||
}): PlanImplementationTarget | null {
|
||||
if (
|
||||
!state.activeThread.id ||
|
||||
|
|
@ -28,7 +28,7 @@ export function implementPlanTargetFromState(state: {
|
|||
) {
|
||||
return null;
|
||||
}
|
||||
return latestImplementablePlanTargetFromItems(messageStreamItems(state.messageStream));
|
||||
return latestImplementablePlanTargetFromItems(threadStreamItems(state.threadStream));
|
||||
}
|
||||
|
||||
export async function implementPlan(host: PlanImplementationHost, itemId: string): Promise<void> {
|
||||
237
src/features/chat/application/turns/runtime-event-plan.ts
Normal file
237
src/features/chat/application/turns/runtime-event-plan.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import { reconcileCompletedTurnItems } from "../../domain/thread-stream/completed-turn-reconciliation";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { attachHookRunsToTurn, completeReasoningItems, upsertThreadStreamItemById } from "../../domain/thread-stream/updates";
|
||||
import { type ChatAction, type ChatState, chatReducer } from "../state/root-reducer";
|
||||
import { threadStreamItems } from "../state/thread-stream";
|
||||
import type { TurnRuntimeEvent } from "./runtime-events";
|
||||
import { activeTurnId, pendingTurnStart as pendingTurnStartForState } from "./turn-state";
|
||||
|
||||
export type TurnRuntimeOutcome =
|
||||
| { type: "turn-started"; threadId: string; turnId: string; recencyAt: number | null }
|
||||
| { type: "turn-completed"; threadId: string; turnId: string; completedSummary: TurnRuntimeEventCompletedSummary };
|
||||
|
||||
type TurnRuntimeEventCompletedSummary = Extract<TurnRuntimeEvent, { type: "turnCompleted" }>["completedSummary"];
|
||||
|
||||
export interface TurnRuntimePlan {
|
||||
actions: readonly ChatAction[];
|
||||
outcomes: readonly TurnRuntimeOutcome[];
|
||||
}
|
||||
|
||||
const EMPTY_PLAN: TurnRuntimePlan = { actions: [], outcomes: [] };
|
||||
|
||||
export function planTurnRuntimeEvents(state: ChatState, events: readonly TurnRuntimeEvent[]): TurnRuntimePlan {
|
||||
let currentState = state;
|
||||
const actions: ChatAction[] = [];
|
||||
const outcomes: TurnRuntimeOutcome[] = [];
|
||||
for (const event of events) {
|
||||
const plan = planTurnRuntimeEvent(currentState, event);
|
||||
actions.push(...plan.actions);
|
||||
outcomes.push(...plan.outcomes);
|
||||
currentState = reducePlannedActions(currentState, plan.actions);
|
||||
}
|
||||
return actions.length === 0 && outcomes.length === 0 ? EMPTY_PLAN : { actions, outcomes };
|
||||
}
|
||||
|
||||
function planTurnRuntimeEvent(state: ChatState, event: TurnRuntimeEvent): TurnRuntimePlan {
|
||||
switch (event.type) {
|
||||
case "assistantDelta":
|
||||
return actionPlan({
|
||||
type: "thread-stream/assistant-delta-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.turnId,
|
||||
delta: event.delta,
|
||||
completeReasoning: event.completeReasoning,
|
||||
});
|
||||
case "planDelta":
|
||||
return actionPlan({
|
||||
type: "thread-stream/plan-delta-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.turnId,
|
||||
delta: event.delta,
|
||||
});
|
||||
case "textDelta":
|
||||
return actionPlan({
|
||||
type: "thread-stream/item-text-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.turnId,
|
||||
label: event.label,
|
||||
delta: event.delta,
|
||||
kind: event.kind,
|
||||
});
|
||||
case "toolOutputDelta":
|
||||
return actionPlan({
|
||||
type: "thread-stream/tool-output-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.turnId,
|
||||
delta: event.delta,
|
||||
fallbackLabel: event.fallbackLabel,
|
||||
});
|
||||
case "itemOutputDelta":
|
||||
return actionPlan({
|
||||
type: "thread-stream/item-output-appended",
|
||||
itemId: event.itemId,
|
||||
turnId: event.turnId,
|
||||
delta: event.delta,
|
||||
kind: event.kind,
|
||||
fallbackText: event.fallbackText,
|
||||
});
|
||||
case "itemUpserted":
|
||||
return actionPlan({ type: "thread-stream/item-upserted", item: event.item });
|
||||
case "itemCompleted":
|
||||
return completedItemPlan(event.item, event.turnId);
|
||||
case "autoReviewUpdated":
|
||||
return autoReviewUpdatedPlan(state, event.item);
|
||||
case "turnStarted":
|
||||
return turnStartedPlan(state, event);
|
||||
case "turnCompleted":
|
||||
return turnCompletedPlan(state, event);
|
||||
case "turnDiffUpdated":
|
||||
return actionPlan({ type: "thread-stream/turn-diff-updated", turnId: event.turnId, diff: event.diff });
|
||||
case "hookRunObserved":
|
||||
return hookRunPlan(state, event);
|
||||
case "requestResolved":
|
||||
return actionPlan({ type: "request/resolved", requestId: event.requestId });
|
||||
case "reviewWarning":
|
||||
return reviewWarningPlan(state, event.item);
|
||||
case "systemNotice":
|
||||
return actionPlan({ type: "thread-stream/system-item-added", item: event.item });
|
||||
}
|
||||
}
|
||||
|
||||
function turnStartedPlan(state: ChatState, event: Extract<TurnRuntimeEvent, { type: "turnStarted" }>): TurnRuntimePlan {
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
type: "turn/started",
|
||||
threadId: event.threadId,
|
||||
turnId: event.turnId,
|
||||
items: threadStreamItemsWithPendingPromptSubmitHooks(state, event.turnId),
|
||||
},
|
||||
],
|
||||
outcomes: [
|
||||
{
|
||||
type: "turn-started",
|
||||
threadId: event.threadId,
|
||||
turnId: event.turnId,
|
||||
recencyAt: event.recencyAt,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function turnCompletedPlan(state: ChatState, event: Extract<TurnRuntimeEvent, { type: "turnCompleted" }>): TurnRuntimePlan {
|
||||
if (activeTurnId(state) !== event.turnId) return EMPTY_PLAN;
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
type: "turn/completed",
|
||||
turnId: event.turnId,
|
||||
status: event.status,
|
||||
items: completeReasoningItems(
|
||||
reconcileCompletedTurnItems({
|
||||
currentItems: threadStreamItems(state.threadStream),
|
||||
completedTurnId: event.turnId,
|
||||
turnItems: event.completedItems,
|
||||
}),
|
||||
event.turnId,
|
||||
),
|
||||
},
|
||||
],
|
||||
outcomes: [
|
||||
{
|
||||
type: "turn-completed",
|
||||
threadId: event.threadId,
|
||||
turnId: event.turnId,
|
||||
completedSummary: event.completedSummary,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function completedItemPlan(item: ThreadStreamItem, turnId: string): TurnRuntimePlan {
|
||||
return {
|
||||
actions: [
|
||||
{ type: "thread-stream/item-upserted", item },
|
||||
...(item.kind === "reasoning" ? ([{ type: "thread-stream/reasoning-completed", turnId: turnId }] satisfies ChatAction[]) : []),
|
||||
],
|
||||
outcomes: [],
|
||||
};
|
||||
}
|
||||
|
||||
function hookRunPlan(state: ChatState, event: Extract<TurnRuntimeEvent, { type: "hookRunObserved" }>): TurnRuntimePlan {
|
||||
const resolvedTurnId = hookTurnId(state, event);
|
||||
const item = resolvedTurnId ? { ...event.item, turnId: resolvedTurnId } : event.item;
|
||||
const currentPendingTurnStart = pendingTurnStartForState(state);
|
||||
let pendingTurnStart = currentPendingTurnStart;
|
||||
if (!resolvedTurnId && currentPendingTurnStart && event.eventName === "userPromptSubmit") {
|
||||
const hookIds = currentPendingTurnStart.promptSubmitHookItemIds;
|
||||
pendingTurnStart = hookIds.includes(item.id)
|
||||
? currentPendingTurnStart
|
||||
: { ...currentPendingTurnStart, promptSubmitHookItemIds: [...hookIds, item.id] };
|
||||
}
|
||||
return actionPlan({
|
||||
type: "turn/pending-start-hook-upserted",
|
||||
item,
|
||||
pendingTurnStart,
|
||||
});
|
||||
}
|
||||
|
||||
function hookTurnId(state: ChatState, event: Extract<TurnRuntimeEvent, { type: "hookRunObserved" }>): string | null {
|
||||
if (event.turnId) return event.turnId;
|
||||
if (event.eventName === "userPromptSubmit" && !pendingTurnStartForState(state)) return activeTurnId(state);
|
||||
return null;
|
||||
}
|
||||
|
||||
function reviewWarningPlan(state: ChatState, item: ThreadStreamItem): TurnRuntimePlan {
|
||||
if (isUnstructuredAutoReviewWarning(item) && hasStructuredAutoReviewResult(threadStreamItems(state.threadStream), activeTurnId(state))) {
|
||||
return EMPTY_PLAN;
|
||||
}
|
||||
return actionPlan({ type: "thread-stream/item-upserted", item });
|
||||
}
|
||||
|
||||
function autoReviewUpdatedPlan(state: ChatState, item: ThreadStreamItem): TurnRuntimePlan {
|
||||
return actionPlan({
|
||||
type: "thread-stream/items-replaced",
|
||||
items: upsertThreadStreamItemById(
|
||||
threadStreamItems(state.threadStream).filter((currentItem) => !isUnstructuredAutoReviewWarning(currentItem)),
|
||||
item,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function threadStreamItemsWithPendingPromptSubmitHooks(state: ChatState, turnId: string): readonly ThreadStreamItem[] {
|
||||
const pending = pendingTurnStartForState(state);
|
||||
const items = threadStreamItems(state.threadStream);
|
||||
if (!pending) return items;
|
||||
return attachHookRunsToTurn(items, turnId, pending.promptSubmitHookItemIds, pending.anchorItemId);
|
||||
}
|
||||
|
||||
function hasStructuredAutoReviewResult(items: readonly ThreadStreamItem[], activeTurnId: string | null): boolean {
|
||||
return items.some(
|
||||
(item) =>
|
||||
item.kind === "reviewResult" &&
|
||||
Boolean(item.turnId) &&
|
||||
(!activeTurnId || item.turnId === activeTurnId) &&
|
||||
isAutoReviewText(item.text),
|
||||
);
|
||||
}
|
||||
|
||||
function isUnstructuredAutoReviewWarning(item: ThreadStreamItem): boolean {
|
||||
return item.kind === "reviewResult" && !item.turnId && isAutoReviewText(item.text);
|
||||
}
|
||||
|
||||
function isAutoReviewText(text: string): boolean {
|
||||
return /^Auto-review\b/i.test(text.trim());
|
||||
}
|
||||
|
||||
function reducePlannedActions(state: ChatState, actions: readonly ChatAction[]): ChatState {
|
||||
return actions.reduce(reducePlannedAction, state);
|
||||
}
|
||||
|
||||
function reducePlannedAction(state: ChatState, action: ChatAction): ChatState {
|
||||
return chatReducer(state, action);
|
||||
}
|
||||
|
||||
function actionPlan(action: ChatAction): TurnRuntimePlan {
|
||||
return { actions: [action], outcomes: [] };
|
||||
}
|
||||
|
|
@ -1,83 +1,83 @@
|
|||
import type { PendingRequestId } from "../../../../domain/pending-requests/model";
|
||||
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
|
||||
type ConversationRuntimeTextItemKind = "tool" | "hook" | "reasoning";
|
||||
type ConversationRuntimeOutputItemKind = "command" | "fileChange";
|
||||
type TurnRuntimeTextItemKind = "tool" | "hook" | "reasoning";
|
||||
type TurnRuntimeOutputItemKind = "command" | "fileChange";
|
||||
|
||||
export type ConversationRuntimeEvent =
|
||||
export type TurnRuntimeEvent =
|
||||
| {
|
||||
type: "assistantDelta";
|
||||
runId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
delta: string;
|
||||
completeReasoning: boolean;
|
||||
}
|
||||
| {
|
||||
type: "planDelta";
|
||||
runId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
delta: string;
|
||||
}
|
||||
| {
|
||||
type: "textDelta";
|
||||
runId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
label: string;
|
||||
delta: string;
|
||||
kind: ConversationRuntimeTextItemKind;
|
||||
kind: TurnRuntimeTextItemKind;
|
||||
}
|
||||
| {
|
||||
type: "toolOutputDelta";
|
||||
runId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
delta: string;
|
||||
fallbackLabel: string;
|
||||
}
|
||||
| {
|
||||
type: "itemOutputDelta";
|
||||
runId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
delta: string;
|
||||
kind: ConversationRuntimeOutputItemKind;
|
||||
kind: TurnRuntimeOutputItemKind;
|
||||
fallbackText: string;
|
||||
}
|
||||
| {
|
||||
type: "itemUpserted";
|
||||
item: MessageStreamItem;
|
||||
item: ThreadStreamItem;
|
||||
}
|
||||
| {
|
||||
type: "itemCompleted";
|
||||
runId: string;
|
||||
item: MessageStreamItem;
|
||||
turnId: string;
|
||||
item: ThreadStreamItem;
|
||||
}
|
||||
| {
|
||||
type: "autoReviewUpdated";
|
||||
item: MessageStreamItem;
|
||||
item: ThreadStreamItem;
|
||||
}
|
||||
| {
|
||||
type: "runStarted";
|
||||
type: "turnStarted";
|
||||
threadId: string;
|
||||
runId: string;
|
||||
turnId: string;
|
||||
recencyAt: number | null;
|
||||
}
|
||||
| {
|
||||
type: "runCompleted";
|
||||
type: "turnCompleted";
|
||||
threadId: string;
|
||||
runId: string;
|
||||
turnId: string;
|
||||
status: string;
|
||||
completedItems: readonly MessageStreamItem[];
|
||||
completedItems: readonly ThreadStreamItem[];
|
||||
completedSummary: ThreadConversationSummary | null;
|
||||
}
|
||||
| {
|
||||
type: "turnDiffUpdated";
|
||||
runId: string;
|
||||
turnId: string;
|
||||
diff: string;
|
||||
}
|
||||
| {
|
||||
type: "hookRunObserved";
|
||||
item: MessageStreamItem;
|
||||
runId: string | null;
|
||||
item: ThreadStreamItem;
|
||||
turnId: string | null;
|
||||
eventName: string;
|
||||
}
|
||||
| {
|
||||
|
|
@ -86,9 +86,9 @@ export type ConversationRuntimeEvent =
|
|||
}
|
||||
| {
|
||||
type: "reviewWarning";
|
||||
item: MessageStreamItem;
|
||||
item: ThreadStreamItem;
|
||||
}
|
||||
| {
|
||||
type: "systemNotice";
|
||||
item: MessageStreamItem;
|
||||
item: ThreadStreamItem;
|
||||
};
|
||||
|
|
@ -7,8 +7,8 @@ import type { Thread } from "../../../../domain/threads/model";
|
|||
import type { ReferencedThreadMetadata } from "../../../../domain/threads/reference";
|
||||
import { resolveThreadSearchQuery } from "../../../../domain/threads/search";
|
||||
import { threadDisplayTitle } from "../../../../domain/threads/title";
|
||||
import type { MessageStreamAuditFact, MessageStreamNoticeSection } from "../../domain/message-stream/items";
|
||||
import { modelOverrideMessage, permissionProfileOverrideMessage, reasoningEffortOverrideMessage } from "../../domain/runtime/labels";
|
||||
import type { ThreadStreamAuditFact, ThreadStreamNoticeSection } from "../../domain/thread-stream/items";
|
||||
import type { ComposerInputSnapshot } from "../composer/input-snapshot";
|
||||
import {
|
||||
type SlashCommandName,
|
||||
|
|
@ -37,7 +37,7 @@ export interface SlashCommandExecutionPorts {
|
|||
};
|
||||
reconnect: () => Promise<void>;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: ThreadStreamNoticeSection[]) => void;
|
||||
runtimeSettings: {
|
||||
toggleFastMode: ChatRuntimeSettingsActions["toggleFastMode"];
|
||||
toggleCollaborationMode: ChatRuntimeSettingsActions["toggleCollaborationMode"];
|
||||
|
|
@ -56,9 +56,9 @@ export interface SlashCommandExecutionPorts {
|
|||
clear: GoalActions["clear"];
|
||||
};
|
||||
statusSummaryLines: () => string[];
|
||||
permissionDetails: () => MessageStreamNoticeSection[];
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
toolInventoryDetails: () => MessageStreamNoticeSection[] | Promise<MessageStreamNoticeSection[]>;
|
||||
permissionDetails: () => ThreadStreamNoticeSection[];
|
||||
connectionDiagnosticDetails: () => ThreadStreamNoticeSection[];
|
||||
toolInventoryDetails: () => ThreadStreamNoticeSection[] | Promise<ThreadStreamNoticeSection[]>;
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
}
|
||||
|
|
@ -423,8 +423,8 @@ function goalUsageError(message: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
function goalDetails(goal: ThreadGoal): MessageStreamNoticeSection[] {
|
||||
const auditFacts: MessageStreamAuditFact[] = [
|
||||
function goalDetails(goal: ThreadGoal): ThreadStreamNoticeSection[] {
|
||||
const auditFacts: ThreadStreamAuditFact[] = [
|
||||
{ key: "status", value: goal.status },
|
||||
{ key: "objective", value: goal.objective },
|
||||
{
|
||||
|
|
@ -451,13 +451,13 @@ function usageError(command: SlashCommandName, message: string): string {
|
|||
return `${definition.command} ${message}. Usage: ${definition.usage}`;
|
||||
}
|
||||
|
||||
function detailsFromLines(lines: string[]): MessageStreamNoticeSection[] {
|
||||
function detailsFromLines(lines: string[]): ThreadStreamNoticeSection[] {
|
||||
const first = lines[0] ?? "";
|
||||
const content = first.includes(": ") ? lines : lines.slice(1);
|
||||
return [{ auditFacts: content.map(lineToRow) }];
|
||||
}
|
||||
|
||||
function lineToRow(line: string): MessageStreamAuditFact {
|
||||
function lineToRow(line: string): ThreadStreamAuditFact {
|
||||
const separator = line.indexOf(": ");
|
||||
if (separator > 0) {
|
||||
return {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamItems } from "../state/message-stream";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import type { ChatState } from "../state/root-reducer";
|
||||
import { threadStreamItems } from "../state/thread-stream";
|
||||
import { activeTurnId, chatTurnBusy, type PendingTurnStart, pendingTurnStart } from "./turn-state";
|
||||
|
||||
export interface SubmissionStateSnapshot {
|
||||
|
|
@ -9,7 +9,7 @@ export interface SubmissionStateSnapshot {
|
|||
activeTurnId: string | null;
|
||||
busy: boolean;
|
||||
listedThreads: readonly Thread[];
|
||||
items: readonly MessageStreamItem[];
|
||||
items: readonly ThreadStreamItem[];
|
||||
pendingTurnStart: PendingTurnStart | null;
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ export function submissionStateSnapshot(state: ChatState): SubmissionStateSnapsh
|
|||
activeTurnId: activeTurnId(state),
|
||||
busy: chatTurnBusy(state),
|
||||
listedThreads: state.threadList.listedThreads,
|
||||
items: messageStreamItems(state.messageStream),
|
||||
items: threadStreamItems(state.threadStream),
|
||||
pendingTurnStart: pendingTurnStart(state),
|
||||
};
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ async function steerCurrentTurn(
|
|||
prunePreservedComposerContext(host, request, { clearSuggestions: true });
|
||||
if (!isCurrentTurn(host, plan.threadId, plan.turnId)) return true;
|
||||
host.stateStore.dispatch({
|
||||
type: "message-stream/item-added",
|
||||
type: "thread-stream/item-added",
|
||||
item: localUserMessageItemFromInput({
|
||||
id: localSteerId,
|
||||
text: prepared.text,
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import type { ExecutionState } from "./items";
|
||||
|
||||
export type MessageStreamExecutionState = Exclude<ExecutionState, null>;
|
||||
export const RUNNING_EXECUTION_STATE: MessageStreamExecutionState = "running";
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import type { MessageStreamItem } from "./items";
|
||||
import { messageStreamSemanticClassifications } from "./semantics/classify";
|
||||
|
||||
export interface ForkCandidate {
|
||||
itemId: string;
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
export interface PlanImplementationTarget {
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamItemsEmptySource {
|
||||
items: readonly MessageStreamItem[];
|
||||
stableItems?: readonly MessageStreamItem[] | undefined;
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
}
|
||||
|
||||
export function messageStreamItemsEmpty(source: MessageStreamItemsEmptySource): boolean {
|
||||
if (!source.stableItems && !source.activeItems) return source.items.length === 0;
|
||||
return messageStreamSegmentsEmpty(source.stableItems ?? [], source.activeItems ?? []);
|
||||
}
|
||||
|
||||
export function messageStreamSegmentsEmpty(stableItems: readonly MessageStreamItem[], activeItems: readonly MessageStreamItem[]): boolean {
|
||||
return stableItems.length === 0 && activeItems.length === 0;
|
||||
}
|
||||
|
||||
export function forkCandidatesFromItems(items: readonly MessageStreamItem[]): readonly ForkCandidate[] {
|
||||
const turnOutcomeItemsByTurn = new Map<string, ForkCandidate>();
|
||||
for (const { item, capabilities } of messageStreamSemanticClassifications(items)) {
|
||||
if (!item.turnId || !capabilities.isTurnOutcome) continue;
|
||||
turnOutcomeItemsByTurn.set(item.turnId, { itemId: item.id, turnId: item.turnId });
|
||||
}
|
||||
return [...turnOutcomeItemsByTurn.values()];
|
||||
}
|
||||
|
||||
export function latestImplementablePlanTargetFromItems(items: readonly MessageStreamItem[]): PlanImplementationTarget | null {
|
||||
const classification = [...messageStreamSemanticClassifications(items)].reverse().find((item) => item.capabilities.canImplementPlan);
|
||||
return classification ? { itemId: classification.item.id } : null;
|
||||
}
|
||||
|
||||
export function isCompletedTurnOutcomeMessage(item: MessageStreamItem): boolean {
|
||||
return messageStreamSemanticClassifications([item])[0]?.capabilities.isTurnOutcome ?? false;
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import type { MessageStreamMeaningEvent, MessageStreamMeaningPlane, MessageStreamSemanticClassification } from "./types";
|
||||
|
||||
function messageStreamHasMeaning(
|
||||
classification: MessageStreamSemanticClassification,
|
||||
plane: MessageStreamMeaningPlane,
|
||||
event: MessageStreamMeaningEvent,
|
||||
): boolean {
|
||||
return classification.meaning.plane === plane && classification.meaning.event === event;
|
||||
}
|
||||
|
||||
export function messageStreamIsTurnInitiator(classification: MessageStreamSemanticClassification): boolean {
|
||||
return (
|
||||
(classification.placement.scope === "turn" || classification.placement.scope === "pendingTurn") &&
|
||||
classification.placement.turnRole === "initiator"
|
||||
);
|
||||
}
|
||||
|
||||
export function messageStreamIsTurnSteer(classification: MessageStreamSemanticClassification): boolean {
|
||||
return (
|
||||
(classification.placement.scope === "turn" || classification.placement.scope === "pendingTurn") &&
|
||||
classification.placement.turnRole === "steer"
|
||||
);
|
||||
}
|
||||
|
||||
export function messageStreamIsWorkspaceResult(classification: MessageStreamSemanticClassification): boolean {
|
||||
return messageStreamHasMeaning(classification, "workspace", "result");
|
||||
}
|
||||
|
||||
export function messageStreamIsCoordinationProgress(classification: MessageStreamSemanticClassification): boolean {
|
||||
return messageStreamHasMeaning(classification, "coordination", "progress");
|
||||
}
|
||||
|
||||
function messageStreamIsPermissionDecision(classification: MessageStreamSemanticClassification): boolean {
|
||||
return messageStreamHasMeaning(classification, "permission", "decision");
|
||||
}
|
||||
|
||||
export function messageStreamIsAutoReviewDecision(classification: MessageStreamSemanticClassification): boolean {
|
||||
const { provenance } = classification;
|
||||
if (!messageStreamIsPermissionDecision(classification) || !provenance) return false;
|
||||
if (provenance.source === "appServer" && provenance.channel === "notification") return provenance.event === "autoReview";
|
||||
if (provenance.source === "panel" && provenance.channel === "notice") return provenance.reason === "parsedAutoReview";
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import type { ExecutionState, MessageStreamItem } from "../items";
|
||||
import type { MessageStreamItemProvenance } from "../provenance";
|
||||
|
||||
type MessageStreamTurnRole = "initiator" | "steer" | "detail" | "outcome";
|
||||
|
||||
export type MessageStreamPlacement =
|
||||
| {
|
||||
scope: "thread";
|
||||
}
|
||||
| {
|
||||
scope: "turn";
|
||||
turnId: string;
|
||||
turnRole: MessageStreamTurnRole;
|
||||
}
|
||||
| {
|
||||
scope: "pendingTurn";
|
||||
turnRole: Extract<MessageStreamTurnRole, "initiator" | "steer">;
|
||||
}
|
||||
| {
|
||||
scope: "item";
|
||||
parentItemId: string;
|
||||
turnId?: string;
|
||||
}
|
||||
| {
|
||||
scope: "panel";
|
||||
};
|
||||
|
||||
export type MessageStreamMeaningPlane =
|
||||
| "dialogue"
|
||||
| "interaction"
|
||||
| "execution"
|
||||
| "workspace"
|
||||
| "coordination"
|
||||
| "permission"
|
||||
| "review"
|
||||
| "context"
|
||||
| "diagnostic";
|
||||
|
||||
export type MessageStreamMeaningEvent =
|
||||
| "request"
|
||||
| "response"
|
||||
| "proposal"
|
||||
| "progress"
|
||||
| "evidence"
|
||||
| "result"
|
||||
| "decision"
|
||||
| "stateChange"
|
||||
| "notice";
|
||||
|
||||
export interface MessageStreamMeaning {
|
||||
plane: MessageStreamMeaningPlane;
|
||||
event: MessageStreamMeaningEvent;
|
||||
}
|
||||
|
||||
export interface MessageStreamLifecycle {
|
||||
state: Exclude<ExecutionState, null>;
|
||||
}
|
||||
|
||||
export interface MessageStreamSemanticCapabilities {
|
||||
canForkFromHere: boolean;
|
||||
canRollbackToPrompt: boolean;
|
||||
canImplementPlan: boolean;
|
||||
isTurnOutcome: boolean;
|
||||
}
|
||||
|
||||
export interface MessageStreamSemanticClassification {
|
||||
item: MessageStreamItem;
|
||||
provenance?: MessageStreamItemProvenance;
|
||||
placement: MessageStreamPlacement;
|
||||
meaning: MessageStreamMeaning;
|
||||
lifecycle?: MessageStreamLifecycle;
|
||||
capabilities: MessageStreamSemanticCapabilities;
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import {
|
|||
type PendingMcpElicitation,
|
||||
type PendingUserInput,
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
import type { MessageStreamItem, MessageStreamUserInputQuestionResult } from "../message-stream/items";
|
||||
import type { ThreadStreamItem, ThreadStreamUserInputQuestionResult } from "../thread-stream/items";
|
||||
|
||||
export function createApprovalResultItem(approval: PendingApproval, action: ApprovalAction): MessageStreamItem {
|
||||
export function createApprovalResultItem(approval: PendingApproval, action: ApprovalAction): ThreadStreamItem {
|
||||
const kind = approvalActionKind(action);
|
||||
return {
|
||||
id: `approval-${String(approval.requestId)}`,
|
||||
|
|
@ -32,7 +32,7 @@ export function createUserInputResultItem(
|
|||
input: PendingUserInput,
|
||||
answers: Record<string, string>,
|
||||
status: "submitted" | "cancelled",
|
||||
): MessageStreamItem {
|
||||
): ThreadStreamItem {
|
||||
const questionCount = input.params.questions.length;
|
||||
const label = questionCount === 1 ? "1 question" : `${String(questionCount)} questions`;
|
||||
const questions = input.params.questions.map((question) => ({
|
||||
|
|
@ -57,7 +57,7 @@ export function createMcpElicitationResultItem(
|
|||
elicitation: PendingMcpElicitation,
|
||||
action: McpElicitationAction,
|
||||
content: Record<string, McpElicitationContentValue> | null,
|
||||
): MessageStreamItem {
|
||||
): ThreadStreamItem {
|
||||
const accepted = action === "accept";
|
||||
return {
|
||||
id: `mcp-elicitation-${action}-${String(elicitation.requestId)}`,
|
||||
|
|
@ -99,7 +99,7 @@ function mcpElicitationResultText(elicitation: PendingMcpElicitation, action: Mc
|
|||
function mcpElicitationResultQuestions(
|
||||
elicitation: PendingMcpElicitation,
|
||||
content: Record<string, McpElicitationContentValue> | null,
|
||||
): readonly MessageStreamUserInputQuestionResult[] {
|
||||
): readonly ThreadStreamUserInputQuestionResult[] {
|
||||
if (elicitation.params.mode === "url") {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import type { MessageStreamItem, MessageStreamMessageItem } from "./items";
|
||||
import type { ThreadStreamDialogueItem, ThreadStreamItem } from "./items";
|
||||
import { isLocalUserMessageId } from "./local-message-ids";
|
||||
import { upsertMessageStreamItemById } from "./updates";
|
||||
import { upsertThreadStreamItemById } from "./updates";
|
||||
|
||||
export interface CompletedTurnReconciliationInput {
|
||||
currentItems: readonly MessageStreamItem[];
|
||||
currentItems: readonly ThreadStreamItem[];
|
||||
completedTurnId: string;
|
||||
turnItems: readonly MessageStreamItem[];
|
||||
turnItems: readonly ThreadStreamItem[];
|
||||
}
|
||||
|
||||
export function reconcileCompletedTurnItems(input: CompletedTurnReconciliationInput): readonly MessageStreamItem[] {
|
||||
export function reconcileCompletedTurnItems(input: CompletedTurnReconciliationInput): readonly ThreadStreamItem[] {
|
||||
const { currentItems, completedTurnId, turnItems } = input;
|
||||
if (turnItems.length === 0) return currentItems;
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ export function reconcileCompletedTurnItems(input: CompletedTurnReconciliationIn
|
|||
.filter((item) => item.turnId === completedTurnId)
|
||||
.filter((item) => !isReconciledOptimisticUserMessage(item, completedTurnId, serverUserClientIds, serverUserFallbackTexts));
|
||||
for (const item of turnItems) {
|
||||
mergedTurnItems = upsertMessageStreamItemById(mergedTurnItems, item);
|
||||
mergedTurnItems = upsertThreadStreamItemById(mergedTurnItems, item);
|
||||
}
|
||||
|
||||
const retainedItems = currentWithServerUsers
|
||||
|
|
@ -33,20 +33,20 @@ export function reconcileCompletedTurnItems(input: CompletedTurnReconciliationIn
|
|||
return [...retainedItems, ...mergedTurnItems];
|
||||
}
|
||||
|
||||
function isUserMessage(item: MessageStreamItem): item is MessageStreamMessageItem & { role: "user" } {
|
||||
return item.kind === "message" && item.role === "user";
|
||||
function isUserMessage(item: ThreadStreamItem): item is ThreadStreamDialogueItem & { role: "user" } {
|
||||
return item.kind === "dialogue" && item.role === "user";
|
||||
}
|
||||
|
||||
function serverUserMessageForOptimisticItem(
|
||||
item: MessageStreamItem,
|
||||
serverUserMessagesByClientId: ReadonlyMap<string, MessageStreamMessageItem & { role: "user" }>,
|
||||
): (MessageStreamMessageItem & { role: "user" }) | null {
|
||||
item: ThreadStreamItem,
|
||||
serverUserMessagesByClientId: ReadonlyMap<string, ThreadStreamDialogueItem & { role: "user" }>,
|
||||
): (ThreadStreamDialogueItem & { role: "user" }) | null {
|
||||
if (!isUserMessage(item) || !isLocalUserMessageId(item.id)) return null;
|
||||
return serverUserMessagesByClientId.get(item.id) ?? null;
|
||||
}
|
||||
|
||||
function isReconciledOptimisticUserMessage(
|
||||
item: MessageStreamItem,
|
||||
item: ThreadStreamItem,
|
||||
completedTurnId: string,
|
||||
serverUserClientIds: Set<string>,
|
||||
serverUserFallbackTexts: Set<string>,
|
||||
|
|
@ -56,7 +56,7 @@ function isReconciledOptimisticUserMessage(
|
|||
}
|
||||
|
||||
function isFallbackOptimisticUserMessageForTurn(
|
||||
item: MessageStreamMessageItem & { role: "user" },
|
||||
item: ThreadStreamDialogueItem & { role: "user" },
|
||||
completedTurnId: string,
|
||||
serverUserFallbackTexts: Set<string>,
|
||||
): boolean {
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import type { ExecutionState } from "./items";
|
||||
|
||||
export type ThreadStreamExecutionState = Exclude<ExecutionState, null>;
|
||||
export const RUNNING_EXECUTION_STATE: ThreadStreamExecutionState = "running";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { truncate } from "../../../../../domain/display/text-preview";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../../../domain/threads/goal";
|
||||
import type { GoalMessageStreamItem } from "../items";
|
||||
import type { GoalThreadStreamItem } from "../items";
|
||||
|
||||
const GOAL_SUMMARY_LIMIT = 140;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ function goalChangeMessage(previous: ThreadGoal | null, next: ThreadGoal | null)
|
|||
return null;
|
||||
}
|
||||
|
||||
export function goalChangeItem(id: string, previous: ThreadGoal | null, next: ThreadGoal | null): GoalMessageStreamItem | null {
|
||||
export function goalChangeItem(id: string, previous: ThreadGoal | null, next: ThreadGoal | null): GoalThreadStreamItem | null {
|
||||
const message = goalChangeMessage(previous, next);
|
||||
if (!message) return null;
|
||||
const objective = next?.objective ?? previous?.objective;
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { RUNNING_EXECUTION_STATE } from "../execution-state";
|
||||
import type { MessageStreamItem, MessageStreamItemKind } from "../items";
|
||||
import type { ThreadStreamItem, ThreadStreamItemKind } from "../items";
|
||||
|
||||
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";
|
||||
const UNKNOWN_STREAMED_COMMAND_CWD = "(unknown)";
|
||||
|
||||
export function streamedTextMessageStreamItem(params: {
|
||||
export function streamedTextThreadStreamItem(params: {
|
||||
id: string;
|
||||
turnId: string;
|
||||
label: string;
|
||||
delta: string;
|
||||
kind: Extract<MessageStreamItemKind, "tool" | "hook" | "reasoning">;
|
||||
}): MessageStreamItem {
|
||||
kind: Extract<ThreadStreamItemKind, "tool" | "hook" | "reasoning">;
|
||||
}): ThreadStreamItem {
|
||||
return {
|
||||
id: params.id,
|
||||
kind: params.kind,
|
||||
|
|
@ -24,12 +24,12 @@ export function streamedTextMessageStreamItem(params: {
|
|||
};
|
||||
}
|
||||
|
||||
export function streamedToolOutputMessageStreamItem(params: {
|
||||
export function streamedToolOutputThreadStreamItem(params: {
|
||||
id: string;
|
||||
turnId: string;
|
||||
output: string;
|
||||
fallbackLabel: string;
|
||||
}): MessageStreamItem {
|
||||
}): ThreadStreamItem {
|
||||
return {
|
||||
id: params.id,
|
||||
kind: "tool",
|
||||
|
|
@ -42,13 +42,13 @@ export function streamedToolOutputMessageStreamItem(params: {
|
|||
};
|
||||
}
|
||||
|
||||
export function streamedItemOutputMessageStreamItem(params: {
|
||||
export function streamedItemOutputThreadStreamItem(params: {
|
||||
id: string;
|
||||
turnId: string;
|
||||
output: string;
|
||||
kind: "command" | "fileChange";
|
||||
fallbackText: string;
|
||||
}): MessageStreamItem {
|
||||
}): ThreadStreamItem {
|
||||
return {
|
||||
id: params.id,
|
||||
kind: params.kind,
|
||||
|
|
@ -71,5 +71,5 @@ export function streamedItemOutputMessageStreamItem(params: {
|
|||
status: "running",
|
||||
executionState: RUNNING_EXECUTION_STATE,
|
||||
}),
|
||||
} as MessageStreamItem;
|
||||
} as ThreadStreamItem;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { MessageStreamItem, MessageStreamNoticeSection } from "../items";
|
||||
import type { ThreadStreamItem, ThreadStreamNoticeSection } from "../items";
|
||||
|
||||
export function createSystemItem(id: string, text: string): MessageStreamItem {
|
||||
export function createSystemItem(id: string, text: string): ThreadStreamItem {
|
||||
return {
|
||||
id,
|
||||
kind: "system",
|
||||
|
|
@ -10,7 +10,7 @@ export function createSystemItem(id: string, text: string): MessageStreamItem {
|
|||
};
|
||||
}
|
||||
|
||||
export function createStructuredSystemItem(id: string, text: string, noticeSections: MessageStreamNoticeSection[]): MessageStreamItem {
|
||||
export function createStructuredSystemItem(id: string, text: string, noticeSections: ThreadStreamNoticeSection[]): ThreadStreamItem {
|
||||
return {
|
||||
id,
|
||||
kind: "system",
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { ACTIVE_FILE_MENTION_NAME, type CodexInputItem } from "../../../../../domain/chat/input";
|
||||
import type { MessageStreamFileMention } from "../items";
|
||||
import type { ThreadStreamFileMention } from "../items";
|
||||
|
||||
const ACTIVE_FILE_DISPLAY_NAME = "Active file";
|
||||
|
||||
export function fileMentionsFromInput(input: readonly CodexInputItem[]): MessageStreamFileMention[] {
|
||||
export function fileMentionsFromInput(input: readonly CodexInputItem[]): ThreadStreamFileMention[] {
|
||||
const seenFilePaths = new Set<string>();
|
||||
const seenActiveNotePaths = new Set<string>();
|
||||
const mentions: MessageStreamFileMention[] = [];
|
||||
const mentions: ThreadStreamFileMention[] = [];
|
||||
for (const item of input) {
|
||||
if (item.type !== "mention") continue;
|
||||
const activeNoteMention = item.name === ACTIVE_FILE_MENTION_NAME;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ReferencedThreadMetadata } from "../../../../domain/threads/reference";
|
||||
import type { MessageStreamItemProvenance } from "./provenance";
|
||||
import type { ThreadStreamItemProvenance } from "./provenance";
|
||||
|
||||
export type MessageStreamItemKind =
|
||||
| "message"
|
||||
export type ThreadStreamItemKind =
|
||||
| "dialogue"
|
||||
| "command"
|
||||
| "fileChange"
|
||||
| "tool"
|
||||
|
|
@ -17,21 +17,21 @@ export type MessageStreamItemKind =
|
|||
| "approvalResult"
|
||||
| "userInputResult"
|
||||
| "reviewResult";
|
||||
type MessageStreamRole = "user" | "assistant" | "system" | "tool";
|
||||
type ThreadStreamRole = "user" | "assistant" | "system" | "tool";
|
||||
export type ExecutionState = "running" | "completed" | "failed" | null;
|
||||
type MessageState = "streaming" | "completed";
|
||||
type DialogueState = "streaming" | "completed";
|
||||
|
||||
interface MessageStreamBase {
|
||||
interface ThreadStreamBase {
|
||||
readonly id: string;
|
||||
readonly kind: MessageStreamItemKind;
|
||||
readonly role: MessageStreamRole;
|
||||
readonly kind: ThreadStreamItemKind;
|
||||
readonly role: ThreadStreamRole;
|
||||
readonly turnId?: string;
|
||||
readonly sourceItemId?: string;
|
||||
readonly provenance?: MessageStreamItemProvenance;
|
||||
readonly provenance?: ThreadStreamItemProvenance;
|
||||
readonly executionState?: ExecutionState;
|
||||
}
|
||||
|
||||
export type MessageStreamPrimaryTarget =
|
||||
export type ThreadStreamPrimaryTarget =
|
||||
| {
|
||||
readonly kind: "path";
|
||||
readonly path: string;
|
||||
|
|
@ -41,61 +41,61 @@ export type MessageStreamPrimaryTarget =
|
|||
readonly value: string;
|
||||
};
|
||||
|
||||
export interface MessageStreamDiagnosticSection {
|
||||
export interface ThreadStreamDiagnosticSection {
|
||||
readonly title: string;
|
||||
readonly body: string;
|
||||
}
|
||||
|
||||
interface MessageStreamWebSearchDetails {
|
||||
interface ThreadStreamWebSearchDetails {
|
||||
readonly action?: string;
|
||||
readonly query?: string;
|
||||
readonly url?: string;
|
||||
readonly pattern?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamImageGenerationDetails {
|
||||
interface ThreadStreamImageGenerationDetails {
|
||||
readonly savedPath?: string;
|
||||
readonly revisedPrompt?: string | null;
|
||||
readonly result?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamHookRunDetails {
|
||||
interface ThreadStreamHookRunDetails {
|
||||
readonly eventName: string;
|
||||
readonly statusMessage?: string;
|
||||
readonly durationMs?: string;
|
||||
readonly entries: readonly { readonly kind: string; readonly text: string }[];
|
||||
}
|
||||
|
||||
export interface MessageStreamAuditFact {
|
||||
export interface ThreadStreamAuditFact {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamNoticeSection {
|
||||
export interface ThreadStreamNoticeSection {
|
||||
readonly title?: string;
|
||||
readonly auditFacts?: readonly MessageStreamAuditFact[];
|
||||
readonly auditFacts?: readonly ThreadStreamAuditFact[];
|
||||
readonly body?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamApprovalResultDetails {
|
||||
interface ThreadStreamApprovalResultDetails {
|
||||
readonly status: string;
|
||||
readonly scope: "session" | "turn";
|
||||
readonly request: string;
|
||||
readonly auditFacts: readonly MessageStreamAuditFact[];
|
||||
readonly auditFacts: readonly ThreadStreamAuditFact[];
|
||||
}
|
||||
|
||||
export interface MessageStreamUserInputQuestionResult {
|
||||
export interface ThreadStreamUserInputQuestionResult {
|
||||
readonly id: string;
|
||||
readonly header: string;
|
||||
readonly question: string;
|
||||
readonly answer?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamReviewResultDetails {
|
||||
readonly auditFacts: readonly MessageStreamAuditFact[];
|
||||
interface ThreadStreamReviewResultDetails {
|
||||
readonly auditFacts: readonly ThreadStreamAuditFact[];
|
||||
}
|
||||
|
||||
export type CommandMessageStreamTarget =
|
||||
export type CommandThreadStreamTarget =
|
||||
| {
|
||||
readonly kind: "read";
|
||||
readonly path?: string;
|
||||
|
|
@ -115,49 +115,52 @@ export type CommandMessageStreamTarget =
|
|||
readonly commandLine: string;
|
||||
};
|
||||
|
||||
interface MessageStreamMessageBase extends MessageStreamBase {
|
||||
readonly kind: "message";
|
||||
interface ThreadStreamDialogueBase extends ThreadStreamBase {
|
||||
readonly kind: "dialogue";
|
||||
readonly role: "user" | "assistant";
|
||||
readonly text: string;
|
||||
readonly clientId?: string;
|
||||
readonly copyText?: string;
|
||||
readonly referencedThread?: ReferencedThreadMetadata;
|
||||
readonly mentionedFiles?: readonly MessageStreamFileMention[];
|
||||
readonly mentionedFiles?: readonly ThreadStreamFileMention[];
|
||||
}
|
||||
|
||||
interface UserMessageStreamItem extends MessageStreamMessageBase {
|
||||
readonly messageKind: "user";
|
||||
interface UserThreadStreamDialogueItem extends ThreadStreamDialogueBase {
|
||||
readonly dialogueKind: "user";
|
||||
readonly role: "user";
|
||||
readonly messageState?: never;
|
||||
readonly dialogueState?: never;
|
||||
}
|
||||
|
||||
interface AssistantResponseMessageStreamItem extends MessageStreamMessageBase {
|
||||
readonly messageKind: "assistantResponse";
|
||||
interface AssistantResponseThreadStreamDialogueItem extends ThreadStreamDialogueBase {
|
||||
readonly dialogueKind: "assistantResponse";
|
||||
readonly role: "assistant";
|
||||
readonly messageState: MessageState;
|
||||
readonly dialogueState: DialogueState;
|
||||
}
|
||||
|
||||
interface ProposedPlanMessageStreamItem extends MessageStreamMessageBase {
|
||||
readonly messageKind: "proposedPlan";
|
||||
interface ProposedPlanThreadStreamDialogueItem extends ThreadStreamDialogueBase {
|
||||
readonly dialogueKind: "proposedPlan";
|
||||
readonly role: "assistant";
|
||||
readonly messageState: MessageState;
|
||||
readonly dialogueState: DialogueState;
|
||||
}
|
||||
|
||||
export type MessageStreamMessageItem = UserMessageStreamItem | AssistantResponseMessageStreamItem | ProposedPlanMessageStreamItem;
|
||||
export type ThreadStreamDialogueItem =
|
||||
| UserThreadStreamDialogueItem
|
||||
| AssistantResponseThreadStreamDialogueItem
|
||||
| ProposedPlanThreadStreamDialogueItem;
|
||||
|
||||
export interface MessageStreamFileMention {
|
||||
export interface ThreadStreamFileMention {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
interface SystemMessageStreamItem extends MessageStreamBase {
|
||||
interface SystemThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "system";
|
||||
readonly role: "system";
|
||||
readonly text: string;
|
||||
readonly noticeSections?: readonly MessageStreamNoticeSection[];
|
||||
readonly noticeSections?: readonly ThreadStreamNoticeSection[];
|
||||
}
|
||||
|
||||
export interface GoalMessageStreamItem extends MessageStreamBase {
|
||||
export interface GoalThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "goal";
|
||||
readonly role: "tool";
|
||||
readonly text: string;
|
||||
|
|
@ -165,32 +168,32 @@ export interface GoalMessageStreamItem extends MessageStreamBase {
|
|||
readonly objective?: string;
|
||||
}
|
||||
|
||||
interface UserInputResultMessageStreamItem extends MessageStreamBase {
|
||||
interface UserInputResultThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "userInputResult";
|
||||
readonly role: "tool";
|
||||
readonly text: string;
|
||||
readonly questions: readonly MessageStreamUserInputQuestionResult[];
|
||||
readonly questions: readonly ThreadStreamUserInputQuestionResult[];
|
||||
}
|
||||
|
||||
export interface ApprovalResultMessageStreamItem extends MessageStreamBase {
|
||||
export interface ApprovalResultThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "approvalResult";
|
||||
readonly role: "tool";
|
||||
readonly text: string;
|
||||
readonly approval: MessageStreamApprovalResultDetails;
|
||||
readonly approval: ThreadStreamApprovalResultDetails;
|
||||
}
|
||||
|
||||
export interface ReviewResultMessageStreamItem extends MessageStreamBase {
|
||||
export interface ReviewResultThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "reviewResult";
|
||||
readonly role: "tool";
|
||||
readonly text: string;
|
||||
readonly review?: MessageStreamReviewResultDetails;
|
||||
readonly review?: ThreadStreamReviewResultDetails;
|
||||
}
|
||||
|
||||
export interface CommandMessageStreamItem extends MessageStreamBase {
|
||||
export interface CommandThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "command";
|
||||
readonly role: "tool";
|
||||
readonly commandAction: "read" | "search" | "listFiles" | "command";
|
||||
readonly commandTarget: CommandMessageStreamTarget;
|
||||
readonly commandTarget: CommandThreadStreamTarget;
|
||||
readonly command: string;
|
||||
readonly cwd: string;
|
||||
readonly status: string;
|
||||
|
|
@ -199,54 +202,54 @@ export interface CommandMessageStreamItem extends MessageStreamBase {
|
|||
readonly output?: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamFileChange {
|
||||
export interface ThreadStreamFileChange {
|
||||
readonly kind: string;
|
||||
readonly path: string;
|
||||
readonly diff: string;
|
||||
}
|
||||
|
||||
export interface FileChangeMessageStreamItem extends MessageStreamBase {
|
||||
export interface FileChangeThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "fileChange";
|
||||
readonly role: "tool";
|
||||
readonly status: string;
|
||||
readonly changes: readonly MessageStreamFileChange[];
|
||||
readonly changes: readonly ThreadStreamFileChange[];
|
||||
readonly output?: string;
|
||||
}
|
||||
|
||||
interface ToolMessageStreamBase extends MessageStreamBase {
|
||||
interface ToolThreadStreamBase extends ThreadStreamBase {
|
||||
readonly role: "tool";
|
||||
readonly text?: string;
|
||||
readonly toolName?: string;
|
||||
readonly primaryTarget?: MessageStreamPrimaryTarget;
|
||||
readonly primaryTarget?: ThreadStreamPrimaryTarget;
|
||||
readonly operation?: string;
|
||||
readonly failureReason?: string;
|
||||
readonly status?: string;
|
||||
readonly output?: string;
|
||||
}
|
||||
|
||||
export interface ToolCallMessageStreamItem extends ToolMessageStreamBase {
|
||||
export interface ToolCallThreadStreamItem extends ToolThreadStreamBase {
|
||||
readonly kind: "tool";
|
||||
readonly diagnostics?: readonly MessageStreamDiagnosticSection[];
|
||||
readonly webSearch?: MessageStreamWebSearchDetails;
|
||||
readonly imageGeneration?: MessageStreamImageGenerationDetails;
|
||||
readonly diagnostics?: readonly ThreadStreamDiagnosticSection[];
|
||||
readonly webSearch?: ThreadStreamWebSearchDetails;
|
||||
readonly imageGeneration?: ThreadStreamImageGenerationDetails;
|
||||
}
|
||||
|
||||
export interface HookMessageStreamItem extends ToolMessageStreamBase {
|
||||
export interface HookThreadStreamItem extends ToolThreadStreamBase {
|
||||
readonly kind: "hook";
|
||||
readonly hookRun?: MessageStreamHookRunDetails;
|
||||
readonly hookRun?: ThreadStreamHookRunDetails;
|
||||
}
|
||||
|
||||
export interface ReasoningMessageStreamItem extends ToolMessageStreamBase {
|
||||
export interface ReasoningThreadStreamItem extends ToolThreadStreamBase {
|
||||
readonly kind: "reasoning";
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
interface ContextCompactionMessageStreamItem extends MessageStreamBase {
|
||||
interface ContextCompactionThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "contextCompaction";
|
||||
readonly role: "tool";
|
||||
}
|
||||
|
||||
interface WaitMessageStreamItem extends MessageStreamBase {
|
||||
interface WaitThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "wait";
|
||||
readonly role: "tool";
|
||||
readonly text: string;
|
||||
|
|
@ -257,7 +260,7 @@ interface TaskProgressStep {
|
|||
readonly status: "pending" | "inProgress" | "completed";
|
||||
}
|
||||
|
||||
export interface TaskProgressMessageStreamItem extends MessageStreamBase {
|
||||
export interface TaskProgressThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "taskProgress";
|
||||
readonly role: "tool";
|
||||
readonly text?: string;
|
||||
|
|
@ -279,7 +282,7 @@ export interface AgentRunSummaryAgent {
|
|||
readonly messagePreview: string | null;
|
||||
}
|
||||
|
||||
export interface AgentMessageStreamItem extends MessageStreamBase {
|
||||
export interface AgentThreadStreamItem extends ThreadStreamBase {
|
||||
readonly kind: "agent";
|
||||
readonly role: "tool";
|
||||
readonly text?: string;
|
||||
|
|
@ -301,19 +304,19 @@ export interface AgentRunSummary {
|
|||
readonly additionalAgents: number;
|
||||
}
|
||||
|
||||
export type MessageStreamItem =
|
||||
| MessageStreamMessageItem
|
||||
| SystemMessageStreamItem
|
||||
| GoalMessageStreamItem
|
||||
| UserInputResultMessageStreamItem
|
||||
| CommandMessageStreamItem
|
||||
| FileChangeMessageStreamItem
|
||||
| ToolCallMessageStreamItem
|
||||
| HookMessageStreamItem
|
||||
| ReasoningMessageStreamItem
|
||||
| WaitMessageStreamItem
|
||||
| ContextCompactionMessageStreamItem
|
||||
| TaskProgressMessageStreamItem
|
||||
| AgentMessageStreamItem
|
||||
| ApprovalResultMessageStreamItem
|
||||
| ReviewResultMessageStreamItem;
|
||||
export type ThreadStreamItem =
|
||||
| ThreadStreamDialogueItem
|
||||
| SystemThreadStreamItem
|
||||
| GoalThreadStreamItem
|
||||
| UserInputResultThreadStreamItem
|
||||
| CommandThreadStreamItem
|
||||
| FileChangeThreadStreamItem
|
||||
| ToolCallThreadStreamItem
|
||||
| HookThreadStreamItem
|
||||
| ReasoningThreadStreamItem
|
||||
| WaitThreadStreamItem
|
||||
| ContextCompactionThreadStreamItem
|
||||
| TaskProgressThreadStreamItem
|
||||
| AgentThreadStreamItem
|
||||
| ApprovalResultThreadStreamItem
|
||||
| ReviewResultThreadStreamItem;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export type MessageStreamItemProvenance =
|
||||
export type ThreadStreamItemProvenance =
|
||||
| {
|
||||
source: "appServer";
|
||||
channel: "turnItem";
|
||||
44
src/features/chat/domain/thread-stream/selectors.ts
Normal file
44
src/features/chat/domain/thread-stream/selectors.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { ThreadStreamItem } from "./items";
|
||||
import { threadStreamSemanticClassifications } from "./semantics/classify";
|
||||
|
||||
export interface ForkCandidate {
|
||||
itemId: string;
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
export interface PlanImplementationTarget {
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface ThreadStreamItemsEmptySource {
|
||||
items: readonly ThreadStreamItem[];
|
||||
stableItems?: readonly ThreadStreamItem[] | undefined;
|
||||
activeItems?: readonly ThreadStreamItem[] | undefined;
|
||||
}
|
||||
|
||||
export function threadStreamItemsEmpty(source: ThreadStreamItemsEmptySource): boolean {
|
||||
if (!source.stableItems && !source.activeItems) return source.items.length === 0;
|
||||
return threadStreamSegmentsEmpty(source.stableItems ?? [], source.activeItems ?? []);
|
||||
}
|
||||
|
||||
export function threadStreamSegmentsEmpty(stableItems: readonly ThreadStreamItem[], activeItems: readonly ThreadStreamItem[]): boolean {
|
||||
return stableItems.length === 0 && activeItems.length === 0;
|
||||
}
|
||||
|
||||
export function forkCandidatesFromItems(items: readonly ThreadStreamItem[]): readonly ForkCandidate[] {
|
||||
const turnOutcomeItemsByTurn = new Map<string, ForkCandidate>();
|
||||
for (const { item, capabilities } of threadStreamSemanticClassifications(items)) {
|
||||
if (!item.turnId || !capabilities.isTurnOutcome) continue;
|
||||
turnOutcomeItemsByTurn.set(item.turnId, { itemId: item.id, turnId: item.turnId });
|
||||
}
|
||||
return [...turnOutcomeItemsByTurn.values()];
|
||||
}
|
||||
|
||||
export function latestImplementablePlanTargetFromItems(items: readonly ThreadStreamItem[]): PlanImplementationTarget | null {
|
||||
const classification = [...threadStreamSemanticClassifications(items)].reverse().find((item) => item.capabilities.canImplementPlan);
|
||||
return classification ? { itemId: classification.item.id } : null;
|
||||
}
|
||||
|
||||
export function isCompletedTurnOutcomeMessage(item: ThreadStreamItem): boolean {
|
||||
return threadStreamSemanticClassifications([item])[0]?.capabilities.isTurnOutcome ?? false;
|
||||
}
|
||||
|
|
@ -3,27 +3,27 @@ import type {
|
|||
AgentRunSummary,
|
||||
AgentRunSummaryAgent,
|
||||
AgentStateSummary,
|
||||
MessageStreamItem,
|
||||
ReasoningMessageStreamItem,
|
||||
TaskProgressMessageStreamItem,
|
||||
ReasoningThreadStreamItem,
|
||||
TaskProgressThreadStreamItem,
|
||||
ThreadStreamItem,
|
||||
} from "../items";
|
||||
import { messageStreamSemanticClassifications } from "./classify";
|
||||
import { messageStreamIsCoordinationProgress } from "./predicates";
|
||||
import type { MessageStreamSemanticClassification } from "./types";
|
||||
import { threadStreamSemanticClassifications } from "./classify";
|
||||
import { threadStreamIsCoordinationProgress } from "./predicates";
|
||||
import type { ThreadStreamSemanticClassification } from "./types";
|
||||
|
||||
const ACTIVE_AGENT_PREVIEW_LIMIT = 96;
|
||||
type AgentRunState = "running" | "completed" | "failed";
|
||||
|
||||
export interface ActiveTurnItemsContext {
|
||||
activeTurnId: string | null;
|
||||
items: readonly MessageStreamItem[];
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
items: readonly ThreadStreamItem[];
|
||||
activeItems?: readonly ThreadStreamItem[] | undefined;
|
||||
}
|
||||
|
||||
export type ActiveTurnLiveItem =
|
||||
| {
|
||||
kind: "taskProgress";
|
||||
item: TaskProgressMessageStreamItem;
|
||||
item: TaskProgressThreadStreamItem;
|
||||
}
|
||||
| {
|
||||
kind: "agentSummary";
|
||||
|
|
@ -36,13 +36,13 @@ export function activeTurnLiveItems(
|
|||
activeTurnId: string,
|
||||
): ActiveTurnLiveItem[] {
|
||||
const items = input.activeItems ?? input.items;
|
||||
const semanticItems = messageStreamSemanticClassifications(items);
|
||||
const semanticItems = threadStreamSemanticClassifications(items);
|
||||
const agentSummaryAnchorId = activeAgentRunSummaryAnchorId(semanticItems, activeTurnId);
|
||||
const agentSummary = agentSummaryAnchorId ? activeAgentRunSummary(items, activeTurnId) : null;
|
||||
|
||||
return semanticItems.flatMap((classification): ActiveTurnLiveItem[] => {
|
||||
const { item } = classification;
|
||||
if (messageStreamItemIsActiveTaskProgress(item, activeTurnId)) {
|
||||
if (threadStreamItemIsActiveTaskProgress(item, activeTurnId)) {
|
||||
return [{ kind: "taskProgress", item }];
|
||||
}
|
||||
if (item.id === agentSummaryAnchorId && agentSummary) {
|
||||
|
|
@ -52,14 +52,11 @@ export function activeTurnLiveItems(
|
|||
});
|
||||
}
|
||||
|
||||
export function messageStreamItemsWithoutActiveTaskProgress(
|
||||
items: readonly MessageStreamItem[],
|
||||
activeTurnId: string,
|
||||
): MessageStreamItem[] {
|
||||
return items.filter((item) => !messageStreamItemIsActiveTaskProgress(item, activeTurnId));
|
||||
export function threadStreamItemsWithoutActiveTaskProgress(items: readonly ThreadStreamItem[], activeTurnId: string): ThreadStreamItem[] {
|
||||
return items.filter((item) => !threadStreamItemIsActiveTaskProgress(item, activeTurnId));
|
||||
}
|
||||
|
||||
function activeAgentRunSummary(items: readonly MessageStreamItem[], activeTurnId: string | null): AgentRunSummary | null {
|
||||
function activeAgentRunSummary(items: readonly ThreadStreamItem[], activeTurnId: string | null): AgentRunSummary | null {
|
||||
if (!activeTurnId) return null;
|
||||
|
||||
const agentStatuses = new Map<string, AgentStateSummary>();
|
||||
|
|
@ -99,7 +96,7 @@ function activeAgentRunSummary(items: readonly MessageStreamItem[], activeTurnId
|
|||
return summary;
|
||||
}
|
||||
|
||||
export function messageStreamReasoningIsActive(item: ReasoningMessageStreamItem, context: ActiveTurnItemsContext): boolean {
|
||||
export function threadStreamReasoningIsActive(item: ReasoningThreadStreamItem, context: ActiveTurnItemsContext): boolean {
|
||||
const activeTurn = context.activeTurnId;
|
||||
if (!activeTurn || item.turnId !== activeTurn) return false;
|
||||
if (item.executionState === "completed") return false;
|
||||
|
|
@ -107,13 +104,13 @@ export function messageStreamReasoningIsActive(item: ReasoningMessageStreamItem,
|
|||
return latestActiveTurnItem?.id === item.id;
|
||||
}
|
||||
|
||||
function messageStreamItemIsActiveTaskProgress(item: MessageStreamItem, activeTurnId: string): item is TaskProgressMessageStreamItem {
|
||||
function threadStreamItemIsActiveTaskProgress(item: ThreadStreamItem, activeTurnId: string): item is TaskProgressThreadStreamItem {
|
||||
return item.kind === "taskProgress" && item.turnId === activeTurnId;
|
||||
}
|
||||
|
||||
function activeAgentRunSummaryAnchorId(items: readonly MessageStreamSemanticClassification[], activeTurnId: string): string | null {
|
||||
function activeAgentRunSummaryAnchorId(items: readonly ThreadStreamSemanticClassification[], activeTurnId: string): string | null {
|
||||
const firstActiveAgent = items.find(
|
||||
(classification) => messageStreamIsCoordinationProgress(classification) && classification.item.turnId === activeTurnId,
|
||||
(classification) => threadStreamIsCoordinationProgress(classification) && classification.item.turnId === activeTurnId,
|
||||
);
|
||||
return firstActiveAgent?.item.id ?? null;
|
||||
}
|
||||
|
|
@ -1,38 +1,38 @@
|
|||
import type { MessageStreamItem } from "../items";
|
||||
import type { ThreadStreamItem } from "../items";
|
||||
import { isLocalSteerMessageClientId } from "../local-message-ids";
|
||||
import type {
|
||||
MessageStreamLifecycle,
|
||||
MessageStreamMeaning,
|
||||
MessageStreamPlacement,
|
||||
MessageStreamSemanticCapabilities,
|
||||
MessageStreamSemanticClassification,
|
||||
ThreadStreamLifecycle,
|
||||
ThreadStreamMeaning,
|
||||
ThreadStreamPlacement,
|
||||
ThreadStreamSemanticCapabilities,
|
||||
ThreadStreamSemanticClassification,
|
||||
} from "./types";
|
||||
|
||||
export function messageStreamSemanticClassifications(items: readonly MessageStreamItem[]): MessageStreamSemanticClassification[] {
|
||||
export function threadStreamSemanticClassifications(items: readonly ThreadStreamItem[]): ThreadStreamSemanticClassification[] {
|
||||
const seenUserMessagesByTurn = new Map<string, number>();
|
||||
return items.map((item) => messageStreamSemanticClassification(item, seenUserMessagesByTurn));
|
||||
return items.map((item) => threadStreamSemanticClassification(item, seenUserMessagesByTurn));
|
||||
}
|
||||
|
||||
function messageStreamSemanticClassification(
|
||||
item: MessageStreamItem,
|
||||
function threadStreamSemanticClassification(
|
||||
item: ThreadStreamItem,
|
||||
seenUserMessagesByTurn: Map<string, number> = new Map<string, number>(),
|
||||
): MessageStreamSemanticClassification {
|
||||
const placement = placementForMessageStreamItem(item, seenUserMessagesByTurn);
|
||||
const meaning = meaningForMessageStreamItem(item);
|
||||
const lifecycle = lifecycleForMessageStreamItem(item);
|
||||
): ThreadStreamSemanticClassification {
|
||||
const placement = placementForThreadStreamItem(item, seenUserMessagesByTurn);
|
||||
const meaning = meaningForThreadStreamItem(item);
|
||||
const lifecycle = lifecycleForThreadStreamItem(item);
|
||||
return {
|
||||
item,
|
||||
...definedProp("provenance", item.provenance),
|
||||
placement,
|
||||
meaning,
|
||||
...definedProp("lifecycle", lifecycle),
|
||||
capabilities: messageStreamSemanticCapabilities({ item, placement, meaning, ...definedProp("lifecycle", lifecycle) }),
|
||||
capabilities: threadStreamSemanticCapabilities({ item, placement, meaning, ...definedProp("lifecycle", lifecycle) }),
|
||||
};
|
||||
}
|
||||
|
||||
function messageStreamSemanticCapabilities(
|
||||
classification: Pick<MessageStreamSemanticClassification, "item" | "placement" | "meaning" | "lifecycle">,
|
||||
): MessageStreamSemanticCapabilities {
|
||||
function threadStreamSemanticCapabilities(
|
||||
classification: Pick<ThreadStreamSemanticClassification, "item" | "placement" | "meaning" | "lifecycle">,
|
||||
): ThreadStreamSemanticCapabilities {
|
||||
const { item, placement, meaning, lifecycle } = classification;
|
||||
const isDialogueOutcome =
|
||||
placement.scope === "turn" &&
|
||||
|
|
@ -44,22 +44,22 @@ function messageStreamSemanticCapabilities(
|
|||
return {
|
||||
canForkFromHere: isDialogueOutcome,
|
||||
canRollbackToPrompt: (placement.scope === "turn" || placement.scope === "pendingTurn") && placement.turnRole === "initiator",
|
||||
canImplementPlan: item.kind === "message" && item.messageKind === "proposedPlan" && lifecycle?.state === "completed",
|
||||
canImplementPlan: item.kind === "dialogue" && item.dialogueKind === "proposedPlan" && lifecycle?.state === "completed",
|
||||
isTurnOutcome: isDialogueOutcome,
|
||||
};
|
||||
}
|
||||
|
||||
function placementForMessageStreamItem(item: MessageStreamItem, seenUserMessagesByTurn: Map<string, number>): MessageStreamPlacement {
|
||||
function placementForThreadStreamItem(item: ThreadStreamItem, seenUserMessagesByTurn: Map<string, number>): ThreadStreamPlacement {
|
||||
if (item.kind === "goal") return { scope: "thread" };
|
||||
if (item.kind === "system") return { scope: "panel" };
|
||||
|
||||
if (item.kind === "message" && item.messageKind === "user") {
|
||||
if (item.kind === "dialogue" && item.dialogueKind === "user") {
|
||||
const turnRole = isSteeringUserMessage(item, seenUserMessagesByTurn) ? "steer" : "initiator";
|
||||
return item.turnId ? { scope: "turn", turnId: item.turnId, turnRole } : { scope: "pendingTurn", turnRole };
|
||||
}
|
||||
|
||||
if (item.kind === "message") {
|
||||
const turnRole = item.messageState === "completed" ? "outcome" : "detail";
|
||||
if (item.kind === "dialogue") {
|
||||
const turnRole = item.dialogueState === "completed" ? "outcome" : "detail";
|
||||
return item.turnId ? { scope: "turn", turnId: item.turnId, turnRole } : { scope: "panel" };
|
||||
}
|
||||
|
||||
|
|
@ -69,11 +69,11 @@ function placementForMessageStreamItem(item: MessageStreamItem, seenUserMessages
|
|||
return { scope: "panel" };
|
||||
}
|
||||
|
||||
function meaningForMessageStreamItem(item: MessageStreamItem): MessageStreamMeaning {
|
||||
function meaningForThreadStreamItem(item: ThreadStreamItem): ThreadStreamMeaning {
|
||||
switch (item.kind) {
|
||||
case "message":
|
||||
if (item.messageKind === "user") return { plane: "dialogue", event: "request" };
|
||||
if (item.messageKind === "proposedPlan") return { plane: "dialogue", event: "proposal" };
|
||||
case "dialogue":
|
||||
if (item.dialogueKind === "user") return { plane: "dialogue", event: "request" };
|
||||
if (item.dialogueKind === "proposedPlan") return { plane: "dialogue", event: "proposal" };
|
||||
return { plane: "dialogue", event: "response" };
|
||||
case "command":
|
||||
case "tool":
|
||||
|
|
@ -102,7 +102,7 @@ function meaningForMessageStreamItem(item: MessageStreamItem): MessageStreamMean
|
|||
return { plane: "diagnostic", event: "notice" };
|
||||
}
|
||||
|
||||
function reviewResultMeaning(item: Extract<MessageStreamItem, { kind: "reviewResult" }>): MessageStreamMeaning {
|
||||
function reviewResultMeaning(item: Extract<ThreadStreamItem, { kind: "reviewResult" }>): ThreadStreamMeaning {
|
||||
if (item.provenance?.source === "appServer" && item.provenance.channel === "notification" && item.provenance.event === "autoReview") {
|
||||
return { plane: "permission", event: "decision" };
|
||||
}
|
||||
|
|
@ -112,19 +112,19 @@ function reviewResultMeaning(item: Extract<MessageStreamItem, { kind: "reviewRes
|
|||
return { plane: "review", event: "result" };
|
||||
}
|
||||
|
||||
function lifecycleForMessageStreamItem(item: MessageStreamItem): MessageStreamLifecycle | undefined {
|
||||
function lifecycleForThreadStreamItem(item: ThreadStreamItem): ThreadStreamLifecycle | undefined {
|
||||
if (item.executionState) return { state: item.executionState };
|
||||
if (item.kind === "message" && item.messageKind !== "user") {
|
||||
return { state: item.messageState === "streaming" ? "running" : "completed" };
|
||||
if (item.kind === "dialogue" && item.dialogueKind !== "user") {
|
||||
return { state: item.dialogueState === "streaming" ? "running" : "completed" };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isSteeringUserMessage(
|
||||
item: Extract<MessageStreamItem, { kind: "message" }>,
|
||||
item: Extract<ThreadStreamItem, { kind: "dialogue" }>,
|
||||
seenUserMessagesByTurn: Map<string, number>,
|
||||
): boolean {
|
||||
if (item.messageKind !== "user") return false;
|
||||
if (item.dialogueKind !== "user") return false;
|
||||
if (item.provenance?.source === "localUser" && item.provenance.interaction === "steer") return true;
|
||||
if (!item.turnId) return false;
|
||||
const seenCount = seenUserMessagesByTurn.get(item.turnId) ?? 0;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import type { ThreadStreamMeaningEvent, ThreadStreamMeaningPlane, ThreadStreamSemanticClassification } from "./types";
|
||||
|
||||
function threadStreamHasMeaning(
|
||||
classification: ThreadStreamSemanticClassification,
|
||||
plane: ThreadStreamMeaningPlane,
|
||||
event: ThreadStreamMeaningEvent,
|
||||
): boolean {
|
||||
return classification.meaning.plane === plane && classification.meaning.event === event;
|
||||
}
|
||||
|
||||
export function threadStreamIsTurnInitiator(classification: ThreadStreamSemanticClassification): boolean {
|
||||
return (
|
||||
(classification.placement.scope === "turn" || classification.placement.scope === "pendingTurn") &&
|
||||
classification.placement.turnRole === "initiator"
|
||||
);
|
||||
}
|
||||
|
||||
export function threadStreamIsTurnSteer(classification: ThreadStreamSemanticClassification): boolean {
|
||||
return (
|
||||
(classification.placement.scope === "turn" || classification.placement.scope === "pendingTurn") &&
|
||||
classification.placement.turnRole === "steer"
|
||||
);
|
||||
}
|
||||
|
||||
export function threadStreamIsWorkspaceResult(classification: ThreadStreamSemanticClassification): boolean {
|
||||
return threadStreamHasMeaning(classification, "workspace", "result");
|
||||
}
|
||||
|
||||
export function threadStreamIsCoordinationProgress(classification: ThreadStreamSemanticClassification): boolean {
|
||||
return threadStreamHasMeaning(classification, "coordination", "progress");
|
||||
}
|
||||
|
||||
function threadStreamIsPermissionDecision(classification: ThreadStreamSemanticClassification): boolean {
|
||||
return threadStreamHasMeaning(classification, "permission", "decision");
|
||||
}
|
||||
|
||||
export function threadStreamIsAutoReviewDecision(classification: ThreadStreamSemanticClassification): boolean {
|
||||
const { provenance } = classification;
|
||||
if (!threadStreamIsPermissionDecision(classification) || !provenance) return false;
|
||||
if (provenance.source === "appServer" && provenance.channel === "notification") return provenance.event === "autoReview";
|
||||
if (provenance.source === "panel" && provenance.channel === "notice") return provenance.reason === "parsedAutoReview";
|
||||
return false;
|
||||
}
|
||||
73
src/features/chat/domain/thread-stream/semantics/types.ts
Normal file
73
src/features/chat/domain/thread-stream/semantics/types.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { ExecutionState, ThreadStreamItem } from "../items";
|
||||
import type { ThreadStreamItemProvenance } from "../provenance";
|
||||
|
||||
type ThreadStreamTurnRole = "initiator" | "steer" | "detail" | "outcome";
|
||||
|
||||
export type ThreadStreamPlacement =
|
||||
| {
|
||||
scope: "thread";
|
||||
}
|
||||
| {
|
||||
scope: "turn";
|
||||
turnId: string;
|
||||
turnRole: ThreadStreamTurnRole;
|
||||
}
|
||||
| {
|
||||
scope: "pendingTurn";
|
||||
turnRole: Extract<ThreadStreamTurnRole, "initiator" | "steer">;
|
||||
}
|
||||
| {
|
||||
scope: "item";
|
||||
parentItemId: string;
|
||||
turnId?: string;
|
||||
}
|
||||
| {
|
||||
scope: "panel";
|
||||
};
|
||||
|
||||
export type ThreadStreamMeaningPlane =
|
||||
| "dialogue"
|
||||
| "interaction"
|
||||
| "execution"
|
||||
| "workspace"
|
||||
| "coordination"
|
||||
| "permission"
|
||||
| "review"
|
||||
| "context"
|
||||
| "diagnostic";
|
||||
|
||||
export type ThreadStreamMeaningEvent =
|
||||
| "request"
|
||||
| "response"
|
||||
| "proposal"
|
||||
| "progress"
|
||||
| "evidence"
|
||||
| "result"
|
||||
| "decision"
|
||||
| "stateChange"
|
||||
| "notice";
|
||||
|
||||
export interface ThreadStreamMeaning {
|
||||
plane: ThreadStreamMeaningPlane;
|
||||
event: ThreadStreamMeaningEvent;
|
||||
}
|
||||
|
||||
export interface ThreadStreamLifecycle {
|
||||
state: Exclude<ExecutionState, null>;
|
||||
}
|
||||
|
||||
export interface ThreadStreamSemanticCapabilities {
|
||||
canForkFromHere: boolean;
|
||||
canRollbackToPrompt: boolean;
|
||||
canImplementPlan: boolean;
|
||||
isTurnOutcome: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadStreamSemanticClassification {
|
||||
item: ThreadStreamItem;
|
||||
provenance?: ThreadStreamItemProvenance;
|
||||
placement: ThreadStreamPlacement;
|
||||
meaning: ThreadStreamMeaning;
|
||||
lifecycle?: ThreadStreamLifecycle;
|
||||
capabilities: ThreadStreamSemanticCapabilities;
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { MessageStreamFileChange, MessageStreamItem } from "./items";
|
||||
import { messageStreamSemanticClassifications } from "./semantics/classify";
|
||||
import { messageStreamIsTurnInitiator } from "./semantics/predicates";
|
||||
import type { ThreadStreamFileChange, ThreadStreamItem } from "./items";
|
||||
import { threadStreamSemanticClassifications } from "./semantics/classify";
|
||||
import { threadStreamIsTurnInitiator } from "./semantics/predicates";
|
||||
|
||||
export function upsertMessageStreamItemById(items: readonly MessageStreamItem[], next: MessageStreamItem): MessageStreamItem[] {
|
||||
export function upsertThreadStreamItemById(items: readonly ThreadStreamItem[], next: ThreadStreamItem): ThreadStreamItem[] {
|
||||
const index = items.findIndex((item) => item.id === next.id);
|
||||
if (index === -1) return [...items, next];
|
||||
const copy = [...items];
|
||||
|
|
@ -13,25 +13,25 @@ export function upsertMessageStreamItemById(items: readonly MessageStreamItem[],
|
|||
...next,
|
||||
output: mergeOutput(previous, next),
|
||||
changes: mergeChanges(previous, next),
|
||||
} as MessageStreamItem;
|
||||
} as ThreadStreamItem;
|
||||
return copy;
|
||||
}
|
||||
|
||||
function mergeOutput(previous: MessageStreamItem, next: MessageStreamItem): string | undefined {
|
||||
function mergeOutput(previous: ThreadStreamItem, next: ThreadStreamItem): string | undefined {
|
||||
const previousOutput = "output" in previous ? previous.output : undefined;
|
||||
const nextOutput = "output" in next ? next.output : undefined;
|
||||
return nextOutput && nextOutput.length > 0 ? nextOutput : previousOutput;
|
||||
}
|
||||
|
||||
function mergeChanges(previous: MessageStreamItem, next: MessageStreamItem): readonly MessageStreamFileChange[] | undefined {
|
||||
function mergeChanges(previous: ThreadStreamItem, next: ThreadStreamItem): readonly ThreadStreamFileChange[] | undefined {
|
||||
const previousChanges = previous.kind === "fileChange" ? previous.changes : undefined;
|
||||
const nextChanges = next.kind === "fileChange" ? next.changes : undefined;
|
||||
return nextChanges && nextChanges.length > 0 ? nextChanges : previousChanges;
|
||||
}
|
||||
|
||||
export function completeReasoningItems(items: readonly MessageStreamItem[], turnId: string): readonly MessageStreamItem[] {
|
||||
export function completeReasoningItems(items: readonly ThreadStreamItem[], turnId: string): readonly ThreadStreamItem[] {
|
||||
let changed = false;
|
||||
const nextItems: MessageStreamItem[] = [];
|
||||
const nextItems: ThreadStreamItem[] = [];
|
||||
for (const item of items) {
|
||||
if (item.kind !== "reasoning" || item.turnId !== turnId) {
|
||||
nextItems.push(item);
|
||||
|
|
@ -42,17 +42,17 @@ export function completeReasoningItems(items: readonly MessageStreamItem[], turn
|
|||
...item,
|
||||
status: "completed",
|
||||
executionState: "completed",
|
||||
} satisfies MessageStreamItem);
|
||||
} satisfies ThreadStreamItem);
|
||||
}
|
||||
return changed ? nextItems : items;
|
||||
}
|
||||
|
||||
export function attachHookRunsToTurn(
|
||||
items: readonly MessageStreamItem[],
|
||||
items: readonly ThreadStreamItem[],
|
||||
turnId: string,
|
||||
hookItemIds: readonly string[],
|
||||
afterItemId?: string | null,
|
||||
): MessageStreamItem[] {
|
||||
): ThreadStreamItem[] {
|
||||
const hookIdSet = new Set(hookItemIds);
|
||||
const attachedHooks = items.filter((item) => hookIdSet.has(item.id)).map((item) => ({ ...item, turnId }));
|
||||
if (attachedHooks.length === 0) return [...items];
|
||||
|
|
@ -65,12 +65,12 @@ export function attachHookRunsToTurn(
|
|||
return [...withoutAttachedHooks.slice(0, insertAfterIndex + 1), ...attachedHooks, ...withoutAttachedHooks.slice(insertAfterIndex + 1)];
|
||||
}
|
||||
|
||||
function lastUserMessageAnchorId(items: readonly MessageStreamItem[], turnId: string): string | null {
|
||||
const anchor = [...messageStreamSemanticClassifications(items)]
|
||||
function lastUserMessageAnchorId(items: readonly ThreadStreamItem[], turnId: string): string | null {
|
||||
const anchor = [...threadStreamSemanticClassifications(items)]
|
||||
.reverse()
|
||||
.find(
|
||||
(classification) =>
|
||||
messageStreamIsTurnInitiator(classification) && (!classification.item.turnId || classification.item.turnId === turnId),
|
||||
threadStreamIsTurnInitiator(classification) && (!classification.item.turnId || classification.item.turnId === turnId),
|
||||
);
|
||||
return anchor?.item.id ?? null;
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import { runtimeSnapshotForChatState } from "../../application/runtime/snapshot"
|
|||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import { resolveRuntimeControls } from "../../domain/runtime/resolution";
|
||||
import { ChatComposerController } from "../../panel/composer-controller";
|
||||
import type { ChatMessageStreamScrollBinding } from "../../panel/message-stream-scroll-binding";
|
||||
import { chatPanelComposerProjection } from "../../panel/surface/composer-projection";
|
||||
import type { ChatThreadStreamScrollBinding } from "../../panel/thread-stream-scroll-binding";
|
||||
import type { ChatPanelEnvironment } from "../contracts";
|
||||
import { createVaultComposerAttachmentHandler } from "../obsidian/composer-attachments.obsidian";
|
||||
import { VaultComposerContextReferenceProvider } from "../obsidian/vault-composer-context-reference-provider.obsidian";
|
||||
|
|
@ -16,7 +16,7 @@ import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle";
|
|||
interface ChatPanelComposerHost {
|
||||
environment: ChatPanelEnvironment;
|
||||
stateStore: ChatStateStore;
|
||||
messageScrollBinding: ChatMessageStreamScrollBinding;
|
||||
threadStreamScrollBinding: ChatThreadStreamScrollBinding;
|
||||
}
|
||||
|
||||
export function createChatComposerController(
|
||||
|
|
@ -53,7 +53,7 @@ export function createChatComposerController(
|
|||
return resolveRuntimeControls(runtimeSnapshotForChatState(current), config).model.effective;
|
||||
},
|
||||
threadScrollFromComposer: (action) => {
|
||||
host.messageScrollBinding.scrollFromComposer(action);
|
||||
host.threadStreamScrollBinding.scrollFromComposer(action);
|
||||
},
|
||||
togglePlan: () => void input.runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void input.runtimeSettings.toggleAutoReview(),
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import type { ChatStateStore } from "../../application/state/store";
|
|||
import type { HistoryController } from "../../application/threads/history-controller";
|
||||
import type { ThreadRenameEditorActions } from "../../application/threads/rename-editor-actions";
|
||||
import type { ChatComposerController } from "../../panel/composer-controller";
|
||||
import type { ChatMessageStreamScrollBinding } from "../../panel/message-stream-scroll-binding";
|
||||
import type { ChatPanelShellParts } from "../../panel/shell.dom";
|
||||
import type { ChatPanelGoalSurface } from "../../panel/surface/goal-projection";
|
||||
import { MessageStreamPresenter } from "../../panel/surface/message-stream-presenter";
|
||||
import { ThreadStreamPresenter } from "../../panel/surface/thread-stream-presenter";
|
||||
import type { ChatPanelToolbarSurface } from "../../panel/surface/toolbar-projection";
|
||||
import type { ChatThreadStreamScrollBinding } from "../../panel/thread-stream-scroll-binding";
|
||||
import { createToolbarUiActions, type ToolbarPanelActions } from "../../panel/toolbar-actions";
|
||||
import { toolbarOutsidePointerHit } from "../../panel/toolbar-hit-test.dom";
|
||||
import type { ChatPanelEnvironment } from "../contracts";
|
||||
|
|
@ -19,7 +19,7 @@ import type { ChatPanelTurnBundle } from "./turn-bundle";
|
|||
interface ChatPanelShellBundleHost {
|
||||
environment: ChatPanelEnvironment;
|
||||
stateStore: ChatStateStore;
|
||||
messageScrollBinding: ChatMessageStreamScrollBinding;
|
||||
threadStreamScrollBinding: ChatThreadStreamScrollBinding;
|
||||
}
|
||||
|
||||
interface ChatPanelShellBundleInput {
|
||||
|
|
@ -85,7 +85,7 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
|
|||
sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut(),
|
||||
actions: goals,
|
||||
};
|
||||
const messageStreamPresenter = new MessageStreamPresenter({
|
||||
const threadStreamPresenter = new ThreadStreamPresenter({
|
||||
obsidian: {
|
||||
app: environment.obsidian.app,
|
||||
owner: environment.obsidian.owner,
|
||||
|
|
@ -97,9 +97,9 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
|
|||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
},
|
||||
scroll: {
|
||||
portBinding: host.messageScrollBinding,
|
||||
portBinding: host.threadStreamScrollBinding,
|
||||
dispose: () => {
|
||||
host.messageScrollBinding.dispose();
|
||||
host.threadStreamScrollBinding.dispose();
|
||||
},
|
||||
},
|
||||
history: {
|
||||
|
|
@ -123,7 +123,7 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
|
|||
actions: toolbarActions,
|
||||
},
|
||||
goal: goalSurface,
|
||||
messageStream: messageStreamPresenter,
|
||||
threadStream: threadStreamPresenter,
|
||||
composer: {
|
||||
presenter: composerController,
|
||||
actions: {
|
||||
|
|
@ -141,7 +141,7 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
|
|||
});
|
||||
},
|
||||
dispose: () => {
|
||||
messageStreamPresenter.dispose();
|
||||
threadStreamPresenter.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { createThreadOperations, type ThreadOperations } from "../../../threads/
|
|||
import { createThreadTitleService, type ThreadTitleService } from "../../../threads/workflows/thread-title-service";
|
||||
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
|
||||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import { messageStreamItems } from "../../application/state/message-stream";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import { threadStreamItems } from "../../application/state/thread-stream";
|
||||
import { type ActiveThreadIdentitySync, createActiveThreadIdentitySync } from "../../application/threads/active-thread-identity-sync";
|
||||
import { type AutoTitleCoordinator, createAutoTitleCoordinator } from "../../application/threads/auto-title-coordinator";
|
||||
import { createGoalActions, createThreadGoalSyncActions } from "../../application/threads/goal-actions";
|
||||
|
|
@ -22,7 +22,7 @@ import type { ChatResumeWorkTracker } from "../../application/threads/resume-wor
|
|||
import { createThreadManagementActions, type ThreadManagementActionsHost } from "../../application/threads/thread-management-actions";
|
||||
import { createThreadNavigationActions } from "../../application/threads/thread-navigation-actions";
|
||||
import type { ThreadStartActions } from "../../application/threads/thread-start-actions";
|
||||
import { threadTitleContextFromMessageStreamItems } from "../../application/threads/title-context";
|
||||
import { threadTitleContextFromThreadStreamItems } from "../../application/threads/title-context";
|
||||
import type { ChatComposerController } from "../../panel/composer-controller";
|
||||
import { createToolbarPanelActions, type ToolbarPanelActions } from "../../panel/toolbar-actions";
|
||||
import type { ChatPanelEnvironment } from "../contracts";
|
||||
|
|
@ -48,7 +48,7 @@ interface ChatPanelThreadHost {
|
|||
environment: ChatPanelEnvironment;
|
||||
stateStore: ChatStateStore;
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
messageScrollBinding: {
|
||||
threadStreamScrollBinding: {
|
||||
showLatest(): void;
|
||||
};
|
||||
getClosing: () => boolean;
|
||||
|
|
@ -118,7 +118,7 @@ export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPan
|
|||
clientAccess: appServer.clientAccess,
|
||||
visibleContext: (threadId) => activeThreadRenameTitleContext(stateStore.getState(), threadId),
|
||||
visibleCompletedTurnContext: (turnId) =>
|
||||
threadTitleContextFromMessageStreamItems(turnId, messageStreamItems(stateStore.getState().messageStream)),
|
||||
threadTitleContextFromThreadStreamItems(turnId, threadStreamItems(stateStore.getState().threadStream)),
|
||||
});
|
||||
const threadOperations = createThreadOperations({
|
||||
clientAccess: appServer.clientAccess,
|
||||
|
|
@ -146,7 +146,7 @@ export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPan
|
|||
historyTransport: appServer.threadHistory,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
showLatestPageAtBottom: () => {
|
||||
host.messageScrollBinding.showLatest();
|
||||
host.threadStreamScrollBinding.showLatest();
|
||||
},
|
||||
setThreadTurnPresence: (hadTurns) => {
|
||||
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
|
||||
|
|
@ -164,7 +164,7 @@ export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPan
|
|||
status.addSystemMessage(text);
|
||||
},
|
||||
addGoalEvent: (item) => {
|
||||
stateStore.dispatch({ type: "message-stream/item-upserted", item });
|
||||
stateStore.dispatch({ type: "thread-stream/item-upserted", item });
|
||||
},
|
||||
refreshLiveState,
|
||||
});
|
||||
|
|
@ -203,7 +203,7 @@ export function createThreadLifecycleBundle(
|
|||
status.addSystemMessage(text);
|
||||
},
|
||||
addGoalEvent: (item) => {
|
||||
host.stateStore.dispatch({ type: "message-stream/item-upserted", item });
|
||||
host.stateStore.dispatch({ type: "thread-stream/item-upserted", item });
|
||||
},
|
||||
refreshLiveState,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import type { ChatInboundHandler } from "../../app-server/inbound/handler";
|
||||
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
|
||||
import {
|
||||
type ConversationTurnActions as ChatPanelConversationTurnActions,
|
||||
createConversationTurnActions,
|
||||
} from "../../application/conversation/composition";
|
||||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import { createPendingRequestActions, type PendingRequestActions } from "../../application/pending-requests/pending-request-actions";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { AutoTitleCoordinator } from "../../application/threads/auto-title-coordinator";
|
||||
import type { ThreadStartActions } from "../../application/threads/thread-start-actions";
|
||||
import type { MessageStreamNoticeSection } from "../../domain/message-stream/items";
|
||||
import { type TurnWorkflowActions as ChatPanelTurnWorkflowActions, createTurnWorkflowActions } from "../../application/turns/composition";
|
||||
import type { ThreadStreamNoticeSection } from "../../domain/thread-stream/items";
|
||||
import type { ChatComposerController } from "../../panel/composer-controller";
|
||||
import type { ChatPanelRuntimeProjection } from "../../panel/runtime-status-projection";
|
||||
import type { ChatPanelEnvironment } from "../contracts";
|
||||
|
|
@ -25,20 +22,20 @@ import type {
|
|||
interface ChatPanelTurnStatus {
|
||||
set: (statusText: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: ThreadStreamNoticeSection[]) => void;
|
||||
}
|
||||
|
||||
interface ChatPanelTurnHost {
|
||||
environment: ChatPanelEnvironment;
|
||||
stateStore: ChatStateStore;
|
||||
messageScrollBinding: {
|
||||
threadStreamScrollBinding: {
|
||||
showLatest(): void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatPanelTurnBundle {
|
||||
pendingRequests: PendingRequestActions;
|
||||
turnActions: ChatPanelConversationTurnActions;
|
||||
turnActions: ChatPanelTurnWorkflowActions;
|
||||
}
|
||||
|
||||
interface ChatPanelTurnInput {
|
||||
|
|
@ -95,7 +92,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
|
|||
addSystemMessage: status.addSystemMessage,
|
||||
setStatus: status.set,
|
||||
});
|
||||
const turnActions = createConversationTurnActions(
|
||||
const turnActions = createTurnWorkflowActions(
|
||||
{
|
||||
stateStore: host.stateStore,
|
||||
localItemIds,
|
||||
|
|
@ -148,7 +145,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
|
|||
},
|
||||
scroll: {
|
||||
showLatest: () => {
|
||||
host.messageScrollBinding.showLatest();
|
||||
host.threadStreamScrollBinding.showLatest();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import type { RestorationController } from "../application/threads/restoration-c
|
|||
import type { ResumeActions } from "../application/threads/resume-actions";
|
||||
import type { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
||||
import { createThreadStartActions } from "../application/threads/thread-start-actions";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items";
|
||||
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../domain/thread-stream/factories/system-items";
|
||||
import type { ThreadStreamNoticeSection } from "../domain/thread-stream/items";
|
||||
import type { ChatComposerController } from "../panel/composer-controller";
|
||||
import type { ChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
|
||||
import type { ChatThreadStreamScrollBinding } from "../panel/thread-stream-scroll-binding";
|
||||
import { createChatComposerController } from "./bundles/composer-bundle";
|
||||
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./bundles/connection-bundle";
|
||||
import { createRuntimeBundle } from "./bundles/runtime-bundle";
|
||||
|
|
@ -57,7 +57,7 @@ export interface ChatPanelSessionGraph {
|
|||
interface ChatPanelSessionStatus {
|
||||
set: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
addStructuredSystemMessage: (text: string, details: ThreadStreamNoticeSection[]) => void;
|
||||
}
|
||||
|
||||
interface ChatPanelSessionGraphHost {
|
||||
|
|
@ -66,7 +66,7 @@ interface ChatPanelSessionGraphHost {
|
|||
deferredTasks: ChatViewDeferredTasks;
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
connectionWork: ConnectionWorkTracker;
|
||||
messageScrollBinding: ChatMessageStreamScrollBinding;
|
||||
threadStreamScrollBinding: ChatThreadStreamScrollBinding;
|
||||
getClosing: () => boolean;
|
||||
viewWindow: () => Window;
|
||||
}
|
||||
|
|
@ -298,11 +298,11 @@ function createSessionStatus(stateStore: ChatStateStore, localItemIds: LocalIdSo
|
|||
dispatch(stateStore, { type: "connection/status-set", statusText, ...(phase ? { phase } : {}) });
|
||||
},
|
||||
addSystemMessage: (text) => {
|
||||
dispatch(stateStore, { type: "message-stream/system-item-added", item: createSystemItem(localItemIds.next("system"), text) });
|
||||
dispatch(stateStore, { type: "thread-stream/system-item-added", item: createSystemItem(localItemIds.next("system"), text) });
|
||||
},
|
||||
addStructuredSystemMessage: (text, details) => {
|
||||
dispatch(stateStore, {
|
||||
type: "message-stream/system-item-added",
|
||||
type: "thread-stream/system-item-added",
|
||||
item: createStructuredSystemItem(localItemIds.next("system"), text, details),
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import type { ChatState } from "../application/state/root-reducer";
|
|||
import { type ChatStateStore, createChatStateStore } from "../application/state/store";
|
||||
import { parseRestoredThreadState, type RestoredThreadPlaceholderState } from "../application/threads/restored-thread-lifecycle";
|
||||
import { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
||||
import { type ChatMessageStreamScrollBinding, createChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
|
||||
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
|
||||
import { type ChatThreadStreamScrollBinding, createChatThreadStreamScrollBinding } from "../panel/thread-stream-scroll-binding";
|
||||
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot, ChatWorkspacePanelTurnLifecycle } from "./contracts";
|
||||
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./session/deferred-work";
|
||||
import { type ChatPanelSessionGraph, createChatPanelSessionGraph } from "./session-graph";
|
||||
|
|
@ -20,7 +20,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
private readonly deferredTasks: ChatViewDeferredTasks;
|
||||
private readonly connectionWork = new ConnectionWorkTracker();
|
||||
private readonly resumeWork = new ChatResumeWorkTracker();
|
||||
private readonly messageScrollBinding: ChatMessageStreamScrollBinding = createChatMessageStreamScrollBinding();
|
||||
private readonly threadStreamScrollBinding: ChatThreadStreamScrollBinding = createChatThreadStreamScrollBinding();
|
||||
private observedAppServerContext: AppServerQueryContext;
|
||||
private opened = false;
|
||||
private closing = false;
|
||||
|
|
@ -251,7 +251,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
deferredTasks: this.deferredTasks,
|
||||
resumeWork: this.resumeWork,
|
||||
connectionWork: this.connectionWork,
|
||||
messageScrollBinding: this.messageScrollBinding,
|
||||
threadStreamScrollBinding: this.threadStreamScrollBinding,
|
||||
getClosing: () => this.closing,
|
||||
viewWindow: () => this.viewWindow(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { runtimeConfigOrDefault } from "../../../domain/runtime/config";
|
||||
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
|
||||
import type { ChatState } from "../application/state/root-reducer";
|
||||
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
|
||||
import { collaborationModeLabel as formatCollaborationModeLabel } from "../domain/runtime/labels";
|
||||
import { resolveRuntimeControls } from "../domain/runtime/resolution";
|
||||
import type { RuntimeSnapshot } from "../domain/runtime/snapshot";
|
||||
import type { ThreadStreamNoticeSection } from "../domain/thread-stream/items";
|
||||
import { appServerDiagnosticSections } from "../presentation/runtime/diagnostic-sections";
|
||||
import { runtimePermissionSections } from "../presentation/runtime/permission-sections";
|
||||
import {
|
||||
|
|
@ -15,12 +15,12 @@ import {
|
|||
import { toolInventoryDiagnosticSections } from "../presentation/runtime/tool-inventory-diagnostic-sections";
|
||||
|
||||
export interface ChatPanelRuntimeProjection {
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
permissionDetails: () => MessageStreamNoticeSection[];
|
||||
connectionDiagnosticDetails: () => ThreadStreamNoticeSection[];
|
||||
permissionDetails: () => ThreadStreamNoticeSection[];
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
statusSummaryLines: () => string[];
|
||||
toolInventoryDetails: () => MessageStreamNoticeSection[];
|
||||
toolInventoryDetails: () => ThreadStreamNoticeSection[];
|
||||
}
|
||||
|
||||
interface ChatPanelRuntimeProjectionInput {
|
||||
|
|
@ -70,7 +70,7 @@ function effortStatusLines(input: ChatPanelRuntimeProjectionInput): string[] {
|
|||
});
|
||||
}
|
||||
|
||||
function connectionDiagnosticDetails(input: ChatPanelRuntimeProjectionInput): MessageStreamNoticeSection[] {
|
||||
function connectionDiagnosticDetails(input: ChatPanelRuntimeProjectionInput): ThreadStreamNoticeSection[] {
|
||||
const state = input.state();
|
||||
const sections = appServerDiagnosticSections({
|
||||
connected: input.connected(),
|
||||
|
|
@ -81,11 +81,11 @@ function connectionDiagnosticDetails(input: ChatPanelRuntimeProjectionInput): Me
|
|||
return noticeSectionsFromDiagnostics(sections);
|
||||
}
|
||||
|
||||
function toolInventoryDetails(input: ChatPanelRuntimeProjectionInput): MessageStreamNoticeSection[] {
|
||||
function toolInventoryDetails(input: ChatPanelRuntimeProjectionInput): ThreadStreamNoticeSection[] {
|
||||
return noticeSectionsFromDiagnostics(toolInventoryDiagnosticSections(input.state().connection.serverDiagnostics));
|
||||
}
|
||||
|
||||
function permissionDetails(input: ChatPanelRuntimeProjectionInput): MessageStreamNoticeSection[] {
|
||||
function permissionDetails(input: ChatPanelRuntimeProjectionInput): ThreadStreamNoticeSection[] {
|
||||
const state = input.state();
|
||||
return noticeSectionsFromDiagnostics(
|
||||
runtimePermissionSections({
|
||||
|
|
@ -97,7 +97,7 @@ function permissionDetails(input: ChatPanelRuntimeProjectionInput): MessageStrea
|
|||
|
||||
function noticeSectionsFromDiagnostics(
|
||||
sections: readonly { title: string; rows: readonly { label: string; value: string }[] }[],
|
||||
): MessageStreamNoticeSection[] {
|
||||
): ThreadStreamNoticeSection[] {
|
||||
return sections.map((section) => ({
|
||||
title: section.title,
|
||||
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { batch, computed, type ReadonlySignal, type Signal, signal } from "@preact/signals";
|
||||
import { explicitThreadName } from "../../../domain/threads/model";
|
||||
import { implementPlanTargetFromState } from "../application/conversation/plan-implementation";
|
||||
import { activeTurnId, chatTurnBusy } from "../application/conversation/turn-state";
|
||||
import { messageItemsHaveThreadTurns, runtimeSnapshotForChatSlices } from "../application/runtime/snapshot";
|
||||
import {
|
||||
type MessageStreamRollbackCandidate,
|
||||
messageStreamActiveItems,
|
||||
messageStreamItems,
|
||||
messageStreamRollbackCandidateFromItems,
|
||||
messageStreamStableItems,
|
||||
} from "../application/state/message-stream";
|
||||
import { runtimeSnapshotForChatSlices, threadStreamItemsHaveThreadTurns } from "../application/runtime/snapshot";
|
||||
import type { ChatState } from "../application/state/root-reducer";
|
||||
import type { MessageStreamItem } from "../domain/message-stream/items";
|
||||
import { type ForkCandidate, forkCandidatesFromItems, type PlanImplementationTarget } from "../domain/message-stream/selectors";
|
||||
import {
|
||||
type ThreadStreamRollbackCandidate,
|
||||
threadStreamActiveItems,
|
||||
threadStreamItems,
|
||||
threadStreamRollbackCandidateFromItems,
|
||||
threadStreamStableItems,
|
||||
} from "../application/state/thread-stream";
|
||||
import { implementPlanTargetFromState } from "../application/turns/plan-implementation";
|
||||
import { activeTurnId, chatTurnBusy } from "../application/turns/turn-state";
|
||||
import type { RuntimeSnapshot } from "../domain/runtime/snapshot";
|
||||
import type { ThreadStreamItem } from "../domain/thread-stream/items";
|
||||
import { type ForkCandidate, forkCandidatesFromItems, type PlanImplementationTarget } from "../domain/thread-stream/selectors";
|
||||
|
||||
export interface ChatPanelShellReadModelBinding {
|
||||
readonly readModel: ChatPanelShellReadModel;
|
||||
|
|
@ -23,7 +23,7 @@ export interface ChatPanelShellReadModelBinding {
|
|||
interface ChatPanelShellReadModel {
|
||||
readonly toolbar: ChatPanelToolbarReadModel;
|
||||
readonly goal: ChatPanelGoalReadModel;
|
||||
readonly messageStream: ChatPanelMessageStreamReadModel;
|
||||
readonly threadStream: ChatPanelThreadStreamReadModel;
|
||||
readonly composer: ChatPanelComposerReadModel;
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ interface ChatPanelShellSignals {
|
|||
activeThread: Signal<ChatState["activeThread"]>;
|
||||
runtime: Signal<ChatState["runtime"]>;
|
||||
turn: Signal<ChatState["turn"]>;
|
||||
messageStream: Signal<ChatState["messageStream"]>;
|
||||
threadStream: Signal<ChatState["threadStream"]>;
|
||||
requests: Signal<ChatState["requests"]>;
|
||||
composer: Signal<ChatState["composer"]>;
|
||||
ui: Signal<ChatState["ui"]>;
|
||||
|
|
@ -42,14 +42,14 @@ interface ChatPanelShellSignals {
|
|||
activeThreadId: ReadonlySignal<ChatState["activeThread"]["id"]>;
|
||||
activeThreadCwd: ReadonlySignal<ChatState["activeThread"]["cwd"]>;
|
||||
activeThreadGoal: ReadonlySignal<ChatState["activeThread"]["goal"]>;
|
||||
messageStreamItems: ReadonlySignal<readonly MessageStreamItem[]>;
|
||||
messageStreamStableItems: ReadonlySignal<readonly MessageStreamItem[]>;
|
||||
messageStreamActiveItems: ReadonlySignal<readonly MessageStreamItem[]>;
|
||||
messageStreamRollbackCandidate: ReadonlySignal<MessageStreamRollbackCandidate | null>;
|
||||
messageStreamForkCandidates: ReadonlySignal<readonly ForkCandidate[]>;
|
||||
messageStreamImplementPlanTarget: ReadonlySignal<PlanImplementationTarget | null>;
|
||||
messageStreamDisclosures: ReadonlySignal<ChatPanelMessageStreamDisclosureState>;
|
||||
messageStreamForkMenuItemId: ReadonlySignal<ChatState["ui"]["messageActionMenu"]["forkMenuItemId"]>;
|
||||
threadStreamItems: ReadonlySignal<readonly ThreadStreamItem[]>;
|
||||
threadStreamStableItems: ReadonlySignal<readonly ThreadStreamItem[]>;
|
||||
threadStreamActiveItems: ReadonlySignal<readonly ThreadStreamItem[]>;
|
||||
threadStreamRollbackCandidate: ReadonlySignal<ThreadStreamRollbackCandidate | null>;
|
||||
threadStreamForkCandidates: ReadonlySignal<readonly ForkCandidate[]>;
|
||||
threadStreamImplementPlanTarget: ReadonlySignal<PlanImplementationTarget | null>;
|
||||
threadStreamDisclosures: ReadonlySignal<ChatPanelThreadStreamDisclosureState>;
|
||||
threadStreamForkMenuItemId: ReadonlySignal<ChatState["ui"]["messageActionMenu"]["forkMenuItemId"]>;
|
||||
hasThreadTurns: ReadonlySignal<boolean>;
|
||||
goalEditor: ReadonlySignal<ChatState["ui"]["goalEditor"]>;
|
||||
goalObjectiveExpanded: ReadonlySignal<ChatState["ui"]["disclosures"]["goalObjectiveExpanded"]>;
|
||||
|
|
@ -97,29 +97,29 @@ export interface ChatPanelGoalReadModel {
|
|||
readonly goalObjectiveExpanded: ReadonlySignal<ChatState["ui"]["disclosures"]["goalObjectiveExpanded"]>;
|
||||
}
|
||||
|
||||
// Message stream read model
|
||||
// Thread stream read model
|
||||
|
||||
export interface ChatPanelMessageStreamReadModel {
|
||||
export interface ChatPanelThreadStreamReadModel {
|
||||
readonly activeThreadId: ReadonlySignal<ChatState["activeThread"]["id"]>;
|
||||
readonly activeThreadCwd: ReadonlySignal<ChatState["activeThread"]["cwd"]>;
|
||||
readonly activeTurnId: ReadonlySignal<string | null>;
|
||||
readonly historyCursor: ReadonlySignal<ChatState["messageStream"]["historyCursor"]>;
|
||||
readonly loadingHistory: ReadonlySignal<ChatState["messageStream"]["loadingHistory"]>;
|
||||
readonly turnDiffs: ReadonlySignal<ChatState["messageStream"]["turnDiffs"]>;
|
||||
readonly items: ReadonlySignal<readonly MessageStreamItem[]>;
|
||||
readonly stableItems: ReadonlySignal<readonly MessageStreamItem[]>;
|
||||
readonly activeItems: ReadonlySignal<readonly MessageStreamItem[]>;
|
||||
readonly historyCursor: ReadonlySignal<ChatState["threadStream"]["historyCursor"]>;
|
||||
readonly loadingHistory: ReadonlySignal<ChatState["threadStream"]["loadingHistory"]>;
|
||||
readonly turnDiffs: ReadonlySignal<ChatState["threadStream"]["turnDiffs"]>;
|
||||
readonly items: ReadonlySignal<readonly ThreadStreamItem[]>;
|
||||
readonly stableItems: ReadonlySignal<readonly ThreadStreamItem[]>;
|
||||
readonly activeItems: ReadonlySignal<readonly ThreadStreamItem[]>;
|
||||
readonly requests: ReadonlySignal<ChatState["requests"]>;
|
||||
readonly disclosures: ReadonlySignal<ChatPanelMessageStreamDisclosureState>;
|
||||
readonly disclosures: ReadonlySignal<ChatPanelThreadStreamDisclosureState>;
|
||||
readonly forkMenuItemId: ReadonlySignal<ChatState["ui"]["messageActionMenu"]["forkMenuItemId"]>;
|
||||
readonly rollbackCandidate: ReadonlySignal<MessageStreamRollbackCandidate | null>;
|
||||
readonly rollbackCandidate: ReadonlySignal<ThreadStreamRollbackCandidate | null>;
|
||||
readonly forkCandidates: ReadonlySignal<readonly ForkCandidate[]>;
|
||||
readonly implementPlanTarget: ReadonlySignal<PlanImplementationTarget | null>;
|
||||
}
|
||||
|
||||
type ChatPanelMessageStreamDisclosureBucket = Exclude<keyof ChatState["ui"]["disclosures"], "goalObjectiveExpanded">;
|
||||
type ChatPanelThreadStreamDisclosureBucket = Exclude<keyof ChatState["ui"]["disclosures"], "goalObjectiveExpanded">;
|
||||
|
||||
type ChatPanelMessageStreamDisclosureState = Pick<ChatState["ui"]["disclosures"], ChatPanelMessageStreamDisclosureBucket>;
|
||||
type ChatPanelThreadStreamDisclosureState = Pick<ChatState["ui"]["disclosures"], ChatPanelThreadStreamDisclosureBucket>;
|
||||
|
||||
// Composer read model
|
||||
|
||||
|
|
@ -147,13 +147,13 @@ export function createChatPanelShellReadModelBinding(initialState: ChatState): C
|
|||
const activeThread = signal(initialState.activeThread);
|
||||
const runtime = signal(initialState.runtime);
|
||||
const turn = signal(initialState.turn);
|
||||
const messageStream = signal(initialState.messageStream);
|
||||
const threadStream = signal(initialState.threadStream);
|
||||
const requests = signal(initialState.requests);
|
||||
const composer = signal(initialState.composer);
|
||||
const ui = signal(initialState.ui);
|
||||
const turnBusy = computed(() => chatTurnBusy({ turn: turn.value }));
|
||||
const messageItems = computed(() => messageStreamItems(messageStream.value));
|
||||
const hasThreadTurns = computed(() => messageItemsHaveThreadTurns(messageItems.value));
|
||||
const streamItems = computed(() => threadStreamItems(threadStream.value));
|
||||
const hasThreadTurns = computed(() => threadStreamItemsHaveThreadTurns(streamItems.value));
|
||||
const activeThreadIdSignal = computed(() => activeThread.value.id);
|
||||
const activeThreadCwd = computed(() => activeThread.value.cwd);
|
||||
const activeThreadTokenUsage = computed(() => activeThread.value.tokenUsage);
|
||||
|
|
@ -164,7 +164,7 @@ export function createChatPanelShellReadModelBinding(initialState: ChatState): C
|
|||
activeThread,
|
||||
runtime,
|
||||
turn,
|
||||
messageStream,
|
||||
threadStream,
|
||||
requests,
|
||||
composer,
|
||||
ui,
|
||||
|
|
@ -173,21 +173,21 @@ export function createChatPanelShellReadModelBinding(initialState: ChatState): C
|
|||
activeThreadId: activeThreadIdSignal,
|
||||
activeThreadCwd,
|
||||
activeThreadGoal,
|
||||
messageStreamItems: messageItems,
|
||||
messageStreamStableItems: computed(() => messageStreamStableItems(messageStream.value)),
|
||||
messageStreamActiveItems: computed(() => messageStreamActiveItems(messageStream.value)),
|
||||
messageStreamRollbackCandidate: computed(() => (turnBusy.value ? null : messageStreamRollbackCandidateFromItems(messageItems.value))),
|
||||
messageStreamForkCandidates: computed(() => (turnBusy.value ? [] : forkCandidatesFromItems(messageItems.value))),
|
||||
messageStreamImplementPlanTarget: computed(() =>
|
||||
threadStreamItems: streamItems,
|
||||
threadStreamStableItems: computed(() => threadStreamStableItems(threadStream.value)),
|
||||
threadStreamActiveItems: computed(() => threadStreamActiveItems(threadStream.value)),
|
||||
threadStreamRollbackCandidate: computed(() => (turnBusy.value ? null : threadStreamRollbackCandidateFromItems(streamItems.value))),
|
||||
threadStreamForkCandidates: computed(() => (turnBusy.value ? [] : forkCandidatesFromItems(streamItems.value))),
|
||||
threadStreamImplementPlanTarget: computed(() =>
|
||||
implementPlanTargetFromState({
|
||||
activeThread: { id: activeThreadIdSignal.value },
|
||||
turn: turn.value,
|
||||
runtime: { pending: { collaborationMode: runtime.value.pending.collaborationMode } },
|
||||
messageStream: messageStream.value,
|
||||
threadStream: threadStream.value,
|
||||
}),
|
||||
),
|
||||
messageStreamDisclosures: createMessageStreamDisclosuresSignal(ui),
|
||||
messageStreamForkMenuItemId: computed(() => ui.value.messageActionMenu.forkMenuItemId),
|
||||
threadStreamDisclosures: createThreadStreamDisclosuresSignal(ui),
|
||||
threadStreamForkMenuItemId: computed(() => ui.value.messageActionMenu.forkMenuItemId),
|
||||
hasThreadTurns,
|
||||
goalEditor: computed(() => ui.value.goalEditor),
|
||||
goalObjectiveExpanded: computed(() => ui.value.disclosures.goalObjectiveExpanded),
|
||||
|
|
@ -228,7 +228,7 @@ function syncShellSignals(signals: ChatPanelShellSignals, nextState: ChatState):
|
|||
if (signals.activeThread.value !== nextState.activeThread) signals.activeThread.value = nextState.activeThread;
|
||||
if (signals.runtime.value !== nextState.runtime) signals.runtime.value = nextState.runtime;
|
||||
if (signals.turn.value !== nextState.turn) signals.turn.value = nextState.turn;
|
||||
if (signals.messageStream.value !== nextState.messageStream) signals.messageStream.value = nextState.messageStream;
|
||||
if (signals.threadStream.value !== nextState.threadStream) signals.threadStream.value = nextState.threadStream;
|
||||
if (signals.requests.value !== nextState.requests) signals.requests.value = nextState.requests;
|
||||
if (signals.composer.value !== nextState.composer) signals.composer.value = nextState.composer;
|
||||
if (signals.ui.value !== nextState.ui) signals.ui.value = nextState.ui;
|
||||
|
|
@ -239,7 +239,7 @@ function shellReadModelFromSignals(signals: ChatPanelShellSignals): ChatPanelShe
|
|||
return {
|
||||
toolbar: toolbarReadModelFromSignals(signals),
|
||||
goal: goalReadModelFromSignals(signals),
|
||||
messageStream: messageStreamReadModelFromSignals(signals),
|
||||
threadStream: threadStreamReadModelFromSignals(signals),
|
||||
composer: composerReadModelFromSignals(signals),
|
||||
};
|
||||
}
|
||||
|
|
@ -289,23 +289,23 @@ function goalReadModelFromSignals(signals: ChatPanelShellSignals): ChatPanelGoal
|
|||
};
|
||||
}
|
||||
|
||||
function messageStreamReadModelFromSignals(signals: ChatPanelShellSignals): ChatPanelMessageStreamReadModel {
|
||||
function threadStreamReadModelFromSignals(signals: ChatPanelShellSignals): ChatPanelThreadStreamReadModel {
|
||||
return {
|
||||
activeThreadId: signals.activeThreadId,
|
||||
activeThreadCwd: signals.activeThreadCwd,
|
||||
activeTurnId: signals.activeTurnId,
|
||||
historyCursor: computed(() => signals.messageStream.value.historyCursor),
|
||||
loadingHistory: computed(() => signals.messageStream.value.loadingHistory),
|
||||
turnDiffs: computed(() => signals.messageStream.value.turnDiffs),
|
||||
items: signals.messageStreamItems,
|
||||
stableItems: signals.messageStreamStableItems,
|
||||
activeItems: signals.messageStreamActiveItems,
|
||||
historyCursor: computed(() => signals.threadStream.value.historyCursor),
|
||||
loadingHistory: computed(() => signals.threadStream.value.loadingHistory),
|
||||
turnDiffs: computed(() => signals.threadStream.value.turnDiffs),
|
||||
items: signals.threadStreamItems,
|
||||
stableItems: signals.threadStreamStableItems,
|
||||
activeItems: signals.threadStreamActiveItems,
|
||||
requests: signals.requests,
|
||||
disclosures: signals.messageStreamDisclosures,
|
||||
forkMenuItemId: signals.messageStreamForkMenuItemId,
|
||||
rollbackCandidate: signals.messageStreamRollbackCandidate,
|
||||
forkCandidates: signals.messageStreamForkCandidates,
|
||||
implementPlanTarget: signals.messageStreamImplementPlanTarget,
|
||||
disclosures: signals.threadStreamDisclosures,
|
||||
forkMenuItemId: signals.threadStreamForkMenuItemId,
|
||||
rollbackCandidate: signals.threadStreamRollbackCandidate,
|
||||
forkCandidates: signals.threadStreamForkCandidates,
|
||||
implementPlanTarget: signals.threadStreamImplementPlanTarget,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -338,8 +338,8 @@ function projectedThreadName(signals: ChatPanelShellSignals, threadId: string):
|
|||
return thread ? explicitThreadName(thread) : null;
|
||||
}
|
||||
|
||||
function createMessageStreamDisclosuresSignal(ui: Signal<ChatState["ui"]>): ReadonlySignal<ChatPanelMessageStreamDisclosureState> {
|
||||
let previous: ChatPanelMessageStreamDisclosureState | null = null;
|
||||
function createThreadStreamDisclosuresSignal(ui: Signal<ChatState["ui"]>): ReadonlySignal<ChatPanelThreadStreamDisclosureState> {
|
||||
let previous: ChatPanelThreadStreamDisclosureState | null = null;
|
||||
return computed(() => {
|
||||
const disclosures = ui.value.disclosures;
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { ToolbarActions } from "../ui/toolbar";
|
|||
import { type ChatPanelShellReadModelBinding, createChatPanelShellReadModelBinding } from "./shell-read-model";
|
||||
import { ChatPanelComposer, type ChatPanelComposerActions, type ChatPanelComposerPresenter } from "./surface/composer-projection";
|
||||
import { ChatPanelGoal, type ChatPanelGoalSurface } from "./surface/goal-projection";
|
||||
import { ChatPanelMessageStream, type ChatPanelMessageStreamPresenter } from "./surface/message-stream-presenter";
|
||||
import { ChatPanelThreadStream, type ChatPanelThreadStreamPresenter } from "./surface/thread-stream-presenter";
|
||||
import { ChatPanelToolbar, type ChatPanelToolbarSurface } from "./surface/toolbar-projection";
|
||||
|
||||
export interface ChatPanelShellParts {
|
||||
|
|
@ -15,7 +15,7 @@ export interface ChatPanelShellParts {
|
|||
actions: ToolbarActions;
|
||||
};
|
||||
goal: ChatPanelGoalSurface;
|
||||
messageStream: ChatPanelMessageStreamPresenter;
|
||||
threadStream: ChatPanelThreadStreamPresenter;
|
||||
composer: {
|
||||
presenter: ChatPanelComposerPresenter;
|
||||
actions: ChatPanelComposerActions;
|
||||
|
|
@ -121,7 +121,7 @@ function ChatPanelShell({
|
|||
<div className="codex-panel__region codex-panel__region--goal" data-codex-panel-shell-region="goal">
|
||||
<ChatPanelGoal model={readModel.goal} surface={parts.goal} />
|
||||
</div>
|
||||
<ChatPanelMessageStream model={readModel.messageStream} presenter={parts.messageStream} />
|
||||
<ChatPanelThreadStream model={readModel.threadStream} presenter={parts.threadStream} />
|
||||
<div className="codex-panel__region codex-panel__region--composer" data-codex-panel-shell-region="composer">
|
||||
<ChatPanelComposer model={readModel.composer} presenter={parts.composer.presenter} actions={parts.composer.actions} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,30 +6,30 @@ import type { TurnDiffViewState } from "../../../turn-diff/model";
|
|||
import type { PendingRequestBlockActions } from "../../application/pending-requests/block";
|
||||
import type { ChatAction } from "../../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { MessageStreamScrollPortBinding } from "../../ui/message-stream/flow-scroll.measure";
|
||||
import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer.obsidian";
|
||||
import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/stream-blocks";
|
||||
import type { ChatPanelMessageStreamReadModel } from "../shell-read-model";
|
||||
import { type ChatMessageStreamSurfaceContext, messageStreamSurfaceProjectionFromModel } from "./message-stream-projection";
|
||||
import type { ThreadStreamScrollPortBinding } from "../../ui/thread-stream/flow-scroll.measure";
|
||||
import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/thread-stream/markdown-renderer.obsidian";
|
||||
import { ThreadStreamViewport, type ThreadStreamViewportState } from "../../ui/thread-stream/stream-blocks";
|
||||
import type { ChatPanelThreadStreamReadModel } from "../shell-read-model";
|
||||
import { type ChatThreadStreamSurfaceContext, threadStreamSurfaceProjectionFromModel } from "./thread-stream-projection";
|
||||
|
||||
export interface ChatPanelMessageStreamPresenter {
|
||||
renderState(model: ChatPanelMessageStreamReadModel): MessageStreamViewportState;
|
||||
export interface ChatPanelThreadStreamPresenter {
|
||||
renderState(model: ChatPanelThreadStreamReadModel): ThreadStreamViewportState;
|
||||
}
|
||||
|
||||
export function ChatPanelMessageStream({
|
||||
export function ChatPanelThreadStream({
|
||||
model,
|
||||
presenter,
|
||||
}: {
|
||||
model: ChatPanelMessageStreamReadModel;
|
||||
presenter: ChatPanelMessageStreamPresenter;
|
||||
model: ChatPanelThreadStreamReadModel;
|
||||
presenter: ChatPanelThreadStreamPresenter;
|
||||
}): UiNode {
|
||||
return h(MessageStreamViewport, {
|
||||
return h(ThreadStreamViewport, {
|
||||
state: presenter.renderState(model),
|
||||
rootAttributes: { "data-codex-panel-shell-region": "message-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
interface ChatMessageStreamActions {
|
||||
interface ChatThreadStreamActions {
|
||||
rollbackThread: (threadId: string) => void;
|
||||
forkThreadFromTurn: (threadId: string, turnId: string, archiveSource: boolean) => void;
|
||||
implementPlan: (itemId: string) => void;
|
||||
|
|
@ -37,48 +37,48 @@ interface ChatMessageStreamActions {
|
|||
openTurnDiff: (state: TurnDiffViewState) => void;
|
||||
}
|
||||
|
||||
interface ChatMessageStreamRequests {
|
||||
interface ChatThreadStreamRequests {
|
||||
pendingActions: () => PendingRequestBlockActions;
|
||||
consumePendingAutoFocus: () => boolean;
|
||||
}
|
||||
|
||||
interface MessageStreamPresenterObsidianContext {
|
||||
interface ThreadStreamPresenterObsidianContext {
|
||||
app: App;
|
||||
owner: Component;
|
||||
}
|
||||
|
||||
interface MessageStreamPresenterStateContext {
|
||||
interface ThreadStreamPresenterStateContext {
|
||||
store: ChatStateStore;
|
||||
}
|
||||
|
||||
interface MessageStreamPresenterWorkspaceContext {
|
||||
interface ThreadStreamPresenterWorkspaceContext {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
interface MessageStreamPresenterScrollContext {
|
||||
portBinding: MessageStreamScrollPortBinding;
|
||||
interface ThreadStreamPresenterScrollContext {
|
||||
portBinding: ThreadStreamScrollPortBinding;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
interface MessageStreamPresenterHistoryContext {
|
||||
interface ThreadStreamPresenterHistoryContext {
|
||||
loadOlderTurns: () => void;
|
||||
}
|
||||
|
||||
export interface MessageStreamPresenterOptions {
|
||||
obsidian: MessageStreamPresenterObsidianContext;
|
||||
state: MessageStreamPresenterStateContext;
|
||||
workspace: MessageStreamPresenterWorkspaceContext;
|
||||
scroll: MessageStreamPresenterScrollContext;
|
||||
history: MessageStreamPresenterHistoryContext;
|
||||
actions: ChatMessageStreamActions;
|
||||
requests: ChatMessageStreamRequests;
|
||||
export interface ThreadStreamPresenterOptions {
|
||||
obsidian: ThreadStreamPresenterObsidianContext;
|
||||
state: ThreadStreamPresenterStateContext;
|
||||
workspace: ThreadStreamPresenterWorkspaceContext;
|
||||
scroll: ThreadStreamPresenterScrollContext;
|
||||
history: ThreadStreamPresenterHistoryContext;
|
||||
actions: ChatThreadStreamActions;
|
||||
requests: ChatThreadStreamRequests;
|
||||
}
|
||||
|
||||
export class MessageStreamPresenter {
|
||||
export class ThreadStreamPresenter {
|
||||
private readonly obsidianMarkdownRenderer: MarkdownMessageRenderer;
|
||||
private readonly surfaceContext: ChatMessageStreamSurfaceContext;
|
||||
private readonly surfaceContext: ChatThreadStreamSurfaceContext;
|
||||
|
||||
constructor(private readonly options: MessageStreamPresenterOptions) {
|
||||
constructor(private readonly options: ThreadStreamPresenterOptions) {
|
||||
this.obsidianMarkdownRenderer = new MarkdownMessageRenderer({
|
||||
app: options.obsidian.app,
|
||||
owner: options.obsidian.owner,
|
||||
|
|
@ -110,8 +110,8 @@ export class MessageStreamPresenter {
|
|||
};
|
||||
}
|
||||
|
||||
renderState(model: ChatPanelMessageStreamReadModel): MessageStreamViewportState {
|
||||
const projection = messageStreamSurfaceProjectionFromModel(model, this.surfaceContext);
|
||||
renderState(model: ChatPanelThreadStreamReadModel): ThreadStreamViewportState {
|
||||
const projection = threadStreamSurfaceProjectionFromModel(model, this.surfaceContext);
|
||||
|
||||
return {
|
||||
blocks: projection.blocks,
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import type { TurnDiffViewState } from "../../../turn-diff/model";
|
||||
import { type PendingRequestBlockActions, pendingRequestBlockStateFromRequestState } from "../../application/pending-requests/block";
|
||||
import type { ChatRequestState } from "../../application/pending-requests/state";
|
||||
import type { MessageStreamRollbackCandidate } from "../../application/state/message-stream";
|
||||
import { type ForkCandidate, messageStreamSegmentsEmpty, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import type { ThreadStreamRollbackCandidate } from "../../application/state/thread-stream";
|
||||
import { pendingRequestsSignature } from "../../domain/pending-requests/signatures";
|
||||
import type { MessageStreamTextActionTargets } from "../../presentation/message-stream/text-view";
|
||||
import { type MessageStreamViewBlock, messageStreamViewBlocks } from "../../presentation/message-stream/view-model";
|
||||
import { type ForkCandidate, type PlanImplementationTarget, threadStreamSegmentsEmpty } from "../../domain/thread-stream/selectors";
|
||||
import { type PendingRequestBlockSnapshot, pendingRequestBlockSnapshotFromState } from "../../presentation/pending-requests/view-model";
|
||||
import type { MessageStreamContext, MessageStreamDisclosureBucket, MessageStreamDisclosureState } from "../../ui/message-stream/context";
|
||||
import type { ChatPanelMessageStreamReadModel } from "../shell-read-model";
|
||||
import type { ThreadStreamTextActionTargets } from "../../presentation/thread-stream/text-view";
|
||||
import { type ThreadStreamViewBlock, threadStreamViewBlocks } from "../../presentation/thread-stream/view-model";
|
||||
import type { ThreadStreamContext, ThreadStreamDisclosureBucket, ThreadStreamDisclosureState } from "../../ui/thread-stream/context";
|
||||
import type { ChatPanelThreadStreamReadModel } from "../shell-read-model";
|
||||
|
||||
interface ChatMessageStreamActions {
|
||||
interface ChatThreadStreamActions {
|
||||
rollbackThread: (threadId: string) => void;
|
||||
forkThreadFromTurn: (threadId: string, turnId: string, archiveSource: boolean) => void;
|
||||
implementPlan: (itemId: string) => void;
|
||||
|
|
@ -18,30 +18,30 @@ interface ChatMessageStreamActions {
|
|||
openTurnDiff: (state: TurnDiffViewState) => void;
|
||||
}
|
||||
|
||||
interface ChatMessageStreamRequests {
|
||||
interface ChatThreadStreamRequests {
|
||||
pendingActions: () => PendingRequestBlockActions;
|
||||
consumePendingAutoFocus: () => boolean;
|
||||
}
|
||||
|
||||
export interface ChatMessageStreamSurfaceContext {
|
||||
export interface ChatThreadStreamSurfaceContext {
|
||||
vaultPath: string;
|
||||
setDisclosureOpen: (bucket: MessageStreamDisclosureBucket, id: string, open: boolean) => void;
|
||||
setDisclosureOpen: (bucket: ThreadStreamDisclosureBucket, id: string, open: boolean) => void;
|
||||
setForkMenuItem: (itemId: string | null) => void;
|
||||
loadOlderTurns: () => void;
|
||||
renderObsidianMarkdown: (element: HTMLElement, text: string) => void;
|
||||
renderStreamMarkdown: (element: HTMLElement, text: string) => void;
|
||||
copyMessageText: (text: string) => void;
|
||||
actions: ChatMessageStreamActions;
|
||||
requests: ChatMessageStreamRequests;
|
||||
actions: ChatThreadStreamActions;
|
||||
requests: ChatThreadStreamRequests;
|
||||
}
|
||||
|
||||
interface MessageStreamStateProjection {
|
||||
interface ThreadStreamStateProjection {
|
||||
activeThreadId: string | null;
|
||||
workspaceRoot: string;
|
||||
disclosures: MessageStreamDisclosureState;
|
||||
disclosures: ThreadStreamDisclosureState;
|
||||
forkMenuItemId: string | null;
|
||||
pendingRequests: { signature: string; snapshot: PendingRequestBlockSnapshot } | null;
|
||||
viewBlocks: readonly MessageStreamViewBlock[];
|
||||
viewBlocks: readonly ThreadStreamViewBlock[];
|
||||
}
|
||||
|
||||
interface PendingRequestSurfaceProjection {
|
||||
|
|
@ -49,26 +49,26 @@ interface PendingRequestSurfaceProjection {
|
|||
readonly snapshot: PendingRequestBlockSnapshot;
|
||||
}
|
||||
|
||||
export interface MessageStreamSurfaceProjection {
|
||||
blocks: readonly MessageStreamViewBlock[];
|
||||
context: MessageStreamContext;
|
||||
export interface ThreadStreamSurfaceProjection {
|
||||
blocks: readonly ThreadStreamViewBlock[];
|
||||
context: ThreadStreamContext;
|
||||
}
|
||||
|
||||
export function messageStreamSurfaceProjectionFromModel(
|
||||
model: ChatPanelMessageStreamReadModel,
|
||||
context: ChatMessageStreamSurfaceContext,
|
||||
): MessageStreamSurfaceProjection {
|
||||
const projection = messageStreamStateProjection(model, context);
|
||||
export function threadStreamSurfaceProjectionFromModel(
|
||||
model: ChatPanelThreadStreamReadModel,
|
||||
context: ChatThreadStreamSurfaceContext,
|
||||
): ThreadStreamSurfaceProjection {
|
||||
const projection = threadStreamStateProjection(model, context);
|
||||
return {
|
||||
blocks: projection.viewBlocks,
|
||||
context: messageStreamContextFromProjection(projection, context),
|
||||
context: threadStreamContextFromProjection(projection, context),
|
||||
};
|
||||
}
|
||||
|
||||
function messageStreamContextFromProjection(
|
||||
projection: MessageStreamStateProjection,
|
||||
context: ChatMessageStreamSurfaceContext,
|
||||
): MessageStreamContext {
|
||||
function threadStreamContextFromProjection(
|
||||
projection: ThreadStreamStateProjection,
|
||||
context: ChatThreadStreamSurfaceContext,
|
||||
): ThreadStreamContext {
|
||||
const pendingRequests = projection.pendingRequests;
|
||||
return {
|
||||
activeThreadId: projection.activeThreadId,
|
||||
|
|
@ -111,20 +111,20 @@ function messageStreamContextFromProjection(
|
|||
};
|
||||
}
|
||||
|
||||
function messageStreamStateProjection(
|
||||
model: ChatPanelMessageStreamReadModel,
|
||||
context: ChatMessageStreamSurfaceContext,
|
||||
): MessageStreamStateProjection {
|
||||
function threadStreamStateProjection(
|
||||
model: ChatPanelThreadStreamReadModel,
|
||||
context: ChatThreadStreamSurfaceContext,
|
||||
): ThreadStreamStateProjection {
|
||||
const stableItems = model.stableItems.value;
|
||||
const activeItems = model.activeItems.value;
|
||||
const disclosures = model.disclosures.value;
|
||||
const workspaceRoot = model.activeThreadCwd.value ?? context.vaultPath;
|
||||
const textActionTargetsByItemId = textActionTargetsForMessageStreamItems(
|
||||
const textActionTargetsByItemId = textActionTargetsForThreadStreamItems(
|
||||
model.rollbackCandidate.value,
|
||||
model.forkCandidates.value,
|
||||
model.implementPlanTarget.value,
|
||||
);
|
||||
const pendingRequests = messageStreamSegmentsEmpty(stableItems, activeItems)
|
||||
const pendingRequests = threadStreamSegmentsEmpty(stableItems, activeItems)
|
||||
? null
|
||||
: pendingRequestSurfaceProjectionFromState(model.requests.value, disclosures.approvalDetails);
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ function messageStreamStateProjection(
|
|||
disclosures,
|
||||
forkMenuItemId: model.forkMenuItemId.value,
|
||||
pendingRequests,
|
||||
viewBlocks: messageStreamViewBlocks({
|
||||
viewBlocks: threadStreamViewBlocks({
|
||||
activeThreadId: model.activeThreadId.value,
|
||||
activeTurnId: model.activeTurnId.value,
|
||||
historyCursor: model.historyCursor.value,
|
||||
|
|
@ -150,12 +150,12 @@ function messageStreamStateProjection(
|
|||
};
|
||||
}
|
||||
|
||||
function textActionTargetsForMessageStreamItems(
|
||||
rollbackCandidate: MessageStreamRollbackCandidate | null,
|
||||
function textActionTargetsForThreadStreamItems(
|
||||
rollbackCandidate: ThreadStreamRollbackCandidate | null,
|
||||
forkCandidates: readonly ForkCandidate[],
|
||||
implementPlanTarget: PlanImplementationTarget | null,
|
||||
): ReadonlyMap<string, MessageStreamTextActionTargets> {
|
||||
const byItemId = new Map<string, MessageStreamTextActionTargets>();
|
||||
): ReadonlyMap<string, ThreadStreamTextActionTargets> {
|
||||
const byItemId = new Map<string, ThreadStreamTextActionTargets>();
|
||||
for (const candidate of forkCandidates) {
|
||||
patchTextActionTargets(byItemId, candidate.itemId, { fork: { itemId: candidate.itemId, turnId: candidate.turnId } });
|
||||
}
|
||||
|
|
@ -187,9 +187,9 @@ function pendingRequestSurfaceProjectionFromState(
|
|||
}
|
||||
|
||||
function patchTextActionTargets(
|
||||
byItemId: Map<string, MessageStreamTextActionTargets>,
|
||||
byItemId: Map<string, ThreadStreamTextActionTargets>,
|
||||
itemId: string,
|
||||
patch: MessageStreamTextActionTargets,
|
||||
patch: ThreadStreamTextActionTargets,
|
||||
): void {
|
||||
byItemId.set(itemId, { ...byItemId.get(itemId), ...patch });
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
import type { ComposerBoundaryScrollAction } from "../application/composer/boundary-scroll";
|
||||
import type {
|
||||
MessageStreamScrollCommand,
|
||||
MessageStreamScrollPort,
|
||||
MessageStreamScrollPortBinding,
|
||||
} from "../ui/message-stream/flow-scroll.measure";
|
||||
ThreadStreamScrollCommand,
|
||||
ThreadStreamScrollPort,
|
||||
ThreadStreamScrollPortBinding,
|
||||
} from "../ui/thread-stream/flow-scroll.measure";
|
||||
|
||||
export interface ChatMessageStreamScrollBinding extends MessageStreamScrollPortBinding {
|
||||
export interface ChatThreadStreamScrollBinding extends ThreadStreamScrollPortBinding {
|
||||
showLatest(): void;
|
||||
scrollFromComposer(action: ComposerBoundaryScrollAction): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function createChatMessageStreamScrollBinding(): ChatMessageStreamScrollBinding {
|
||||
let scrollPort: MessageStreamScrollPort | null = null;
|
||||
export function createChatThreadStreamScrollBinding(): ChatThreadStreamScrollBinding {
|
||||
let scrollPort: ThreadStreamScrollPort | null = null;
|
||||
|
||||
const dispatch = (command: MessageStreamScrollCommand): void => {
|
||||
const dispatch = (command: ThreadStreamScrollCommand): void => {
|
||||
scrollPort?.dispatchScrollCommand(command);
|
||||
};
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ export function createToolbarUiActions(deps: ToolbarUiActionDependencies): Toolb
|
|||
startNewThread: () => {
|
||||
void deps.navigation.startNewThread();
|
||||
},
|
||||
compactConversation: () => {
|
||||
compactContext: () => {
|
||||
void deps.threadActions.compactActiveThread();
|
||||
},
|
||||
setGoal: () => {
|
||||
|
|
|
|||
|
|
@ -1,303 +0,0 @@
|
|||
import type { AgentRunSummary, MessageStreamItem, TaskProgressMessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamItemsEmpty } from "../../domain/message-stream/selectors";
|
||||
import { activeTurnLiveItems, messageStreamItemsWithoutActiveTaskProgress } from "../../domain/message-stream/semantics/active-turn";
|
||||
import type { MessageStreamSemanticClassification } from "../../domain/message-stream/semantics/types";
|
||||
import type { PendingRequestBlockSnapshot } from "../pending-requests/view-model";
|
||||
import { type DetailView, detailView } from "./detail-view";
|
||||
import { type MessageStreamItemAnnotations, type MessageStreamLayoutBlock, messageStreamLayoutBlocks } from "./layout";
|
||||
import { type AgentRunSummaryView, agentRunSummaryView, type MessageStreamStatusView, messageStreamStatusView } from "./status-view";
|
||||
import { type MessageStreamTextActionTargets, type MessageStreamTextView, messageStreamTextView } from "./text-view";
|
||||
|
||||
interface PendingRequestMessageStreamBlockInput {
|
||||
signature: string;
|
||||
snapshot: PendingRequestBlockSnapshot;
|
||||
}
|
||||
|
||||
export interface MessageStreamPresentationBlockInput {
|
||||
activeThreadId: string | null;
|
||||
activeTurnId: string | null;
|
||||
historyCursor: string | null;
|
||||
loadingHistory: boolean;
|
||||
items: readonly MessageStreamItem[];
|
||||
stableItems?: readonly MessageStreamItem[] | undefined;
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
workspaceRoot?: string | null | undefined;
|
||||
turnDiffs?: ReadonlyMap<string, string> | undefined;
|
||||
textActionTargetsByItemId?: ReadonlyMap<string, MessageStreamTextActionTargets> | undefined;
|
||||
pendingRequests?: PendingRequestMessageStreamBlockInput | null | undefined;
|
||||
}
|
||||
|
||||
type MessageStreamPresentationBlock =
|
||||
| {
|
||||
kind: "historyBar";
|
||||
key: "history-bar";
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "empty";
|
||||
key: "empty";
|
||||
}
|
||||
| {
|
||||
kind: "item";
|
||||
key: string;
|
||||
block: Extract<MessageStreamLayoutBlock, { type: "item" }>;
|
||||
}
|
||||
| {
|
||||
kind: "activityGroup";
|
||||
key: string;
|
||||
block: Extract<MessageStreamLayoutBlock, { type: "activityGroup" }>;
|
||||
}
|
||||
| {
|
||||
kind: "liveTask";
|
||||
key: string;
|
||||
item: TaskProgressMessageStreamItem;
|
||||
}
|
||||
| {
|
||||
kind: "liveAgentSummary";
|
||||
key: string;
|
||||
summary: AgentRunSummary;
|
||||
}
|
||||
| {
|
||||
kind: "pendingRequests";
|
||||
key: "pending-requests";
|
||||
signature: string;
|
||||
snapshot: PendingRequestBlockSnapshot;
|
||||
};
|
||||
|
||||
type MessageStreamPresentationBlockSource = (input: MessageStreamPresentationBlockInput) => readonly MessageStreamPresentationBlock[];
|
||||
type MessageStreamRenderFamily = "text" | "detail" | "status";
|
||||
|
||||
export type MessageStreamRenderedItemView =
|
||||
| {
|
||||
kind: "text";
|
||||
view: MessageStreamTextView;
|
||||
}
|
||||
| {
|
||||
kind: "detail";
|
||||
view: DetailView;
|
||||
}
|
||||
| {
|
||||
kind: "status";
|
||||
view: MessageStreamStatusView;
|
||||
};
|
||||
|
||||
export type MessageStreamActivityItemView =
|
||||
| ({
|
||||
type: "item";
|
||||
id: string;
|
||||
} & MessageStreamRenderedItemView)
|
||||
| {
|
||||
type: "steering";
|
||||
id: string;
|
||||
label: string;
|
||||
text: string;
|
||||
sourceItemId: string;
|
||||
};
|
||||
|
||||
export type MessageStreamViewBlock =
|
||||
| {
|
||||
kind: "historyBar";
|
||||
key: "history-bar";
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "empty";
|
||||
key: "empty";
|
||||
}
|
||||
| ({
|
||||
key: string;
|
||||
} & MessageStreamRenderedItemView)
|
||||
| {
|
||||
kind: "activityGroup";
|
||||
key: string;
|
||||
id: string;
|
||||
turnId: string;
|
||||
summary: string;
|
||||
items: MessageStreamActivityItemView[];
|
||||
}
|
||||
| {
|
||||
kind: "liveAgentSummary";
|
||||
key: string;
|
||||
view: AgentRunSummaryView;
|
||||
}
|
||||
| {
|
||||
kind: "pendingRequests";
|
||||
key: "pending-requests";
|
||||
signature: string;
|
||||
snapshot: PendingRequestBlockSnapshot;
|
||||
};
|
||||
|
||||
export function messageStreamViewBlocks(input: MessageStreamPresentationBlockInput): MessageStreamViewBlock[] {
|
||||
return messageStreamPresentationBlocks(input).map((block) => messageStreamViewBlockFromPresentationBlock(block, input));
|
||||
}
|
||||
|
||||
function messageStreamPresentationBlocks(input: MessageStreamPresentationBlockInput): MessageStreamPresentationBlock[] {
|
||||
const headerBlocks = historyPresentationBlocks(input);
|
||||
if (messageStreamItemsEmpty(input)) {
|
||||
return [...headerBlocks, { kind: "empty", key: "empty" }];
|
||||
}
|
||||
|
||||
return [
|
||||
...headerBlocks,
|
||||
...collectPresentationBlocks(input, [layoutPresentationBlocks, activeTurnPresentationBlocks, pendingRequestPresentationBlocks]),
|
||||
];
|
||||
}
|
||||
|
||||
function collectPresentationBlocks(
|
||||
input: MessageStreamPresentationBlockInput,
|
||||
sources: readonly MessageStreamPresentationBlockSource[],
|
||||
): MessageStreamPresentationBlock[] {
|
||||
return sources.flatMap((source) => source(input));
|
||||
}
|
||||
|
||||
function historyPresentationBlocks(input: MessageStreamPresentationBlockInput): readonly MessageStreamPresentationBlock[] {
|
||||
if (!input.activeThreadId || !input.historyCursor) return [];
|
||||
return [{ kind: "historyBar", key: "history-bar", loadingHistory: input.loadingHistory }];
|
||||
}
|
||||
|
||||
function layoutPresentationBlocks(input: MessageStreamPresentationBlockInput): readonly MessageStreamPresentationBlock[] {
|
||||
return layoutBlocksForInput(input).map(presentationBlockFromLayoutBlock);
|
||||
}
|
||||
|
||||
function presentationBlockFromLayoutBlock(block: MessageStreamLayoutBlock): MessageStreamPresentationBlock {
|
||||
if (block.type === "item") return { kind: "item", key: `item:${block.item.id}`, block };
|
||||
return { kind: "activityGroup", key: `activity:${block.id}`, block };
|
||||
}
|
||||
|
||||
function activeTurnPresentationBlocks(input: MessageStreamPresentationBlockInput): readonly MessageStreamPresentationBlock[] {
|
||||
if (!input.activeTurnId) return [];
|
||||
return activeTurnLiveBlocks(input, input.activeTurnId);
|
||||
}
|
||||
|
||||
function pendingRequestPresentationBlocks(input: MessageStreamPresentationBlockInput): readonly MessageStreamPresentationBlock[] {
|
||||
if (!input.pendingRequests?.signature) return [];
|
||||
return [
|
||||
{
|
||||
kind: "pendingRequests",
|
||||
key: "pending-requests",
|
||||
signature: input.pendingRequests.signature,
|
||||
snapshot: input.pendingRequests.snapshot,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function layoutBlocksForInput(input: MessageStreamPresentationBlockInput): MessageStreamLayoutBlock[] {
|
||||
const { activeTurnId } = input;
|
||||
if (!activeTurnId || !input.stableItems || !input.activeItems) {
|
||||
const streamItems = activeTurnId ? messageStreamItemsWithoutActiveTaskProgress(input.items, activeTurnId) : input.items;
|
||||
return messageStreamLayoutBlocks(streamItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
}
|
||||
const stableBlocks = messageStreamLayoutBlocks(input.stableItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
const activeBlocks = messageStreamLayoutBlocks(
|
||||
messageStreamItemsWithoutActiveTaskProgress(input.activeItems, activeTurnId),
|
||||
activeTurnId,
|
||||
input.workspaceRoot,
|
||||
input.turnDiffs,
|
||||
);
|
||||
return [...stableBlocks, ...activeBlocks];
|
||||
}
|
||||
|
||||
function activeTurnLiveBlocks(
|
||||
input: Pick<MessageStreamPresentationBlockInput, "items" | "activeItems">,
|
||||
activeTurnId: string,
|
||||
): MessageStreamPresentationBlock[] {
|
||||
return activeTurnLiveItems(input, activeTurnId).map((item): MessageStreamPresentationBlock => {
|
||||
if (item.kind === "taskProgress") {
|
||||
return {
|
||||
kind: "liveTask",
|
||||
key: `live-task:${item.item.id}`,
|
||||
item: item.item,
|
||||
};
|
||||
}
|
||||
return { kind: "liveAgentSummary", key: `live-agents:${activeTurnId}`, summary: item.summary };
|
||||
});
|
||||
}
|
||||
|
||||
function messageStreamViewBlockFromPresentationBlock(
|
||||
block: MessageStreamPresentationBlock,
|
||||
input: MessageStreamPresentationBlockInput,
|
||||
): MessageStreamViewBlock {
|
||||
if (block.kind === "historyBar" || block.kind === "empty") return block;
|
||||
if (block.kind === "pendingRequests") return block;
|
||||
if (block.kind === "liveAgentSummary") return { kind: "liveAgentSummary", key: block.key, view: agentRunSummaryView(block.summary) };
|
||||
if (block.kind === "liveTask") {
|
||||
return { kind: "status", key: block.key, view: messageStreamStatusView(block.item, statusViewContext(input)) };
|
||||
}
|
||||
if (block.kind === "activityGroup") {
|
||||
return {
|
||||
kind: "activityGroup",
|
||||
key: block.key,
|
||||
id: block.block.id,
|
||||
turnId: block.block.turnId,
|
||||
summary: block.block.summary,
|
||||
items: block.block.items.map((activity) => messageStreamActivityItemView(activity, input)),
|
||||
};
|
||||
}
|
||||
return { key: block.key, ...messageStreamRenderedItemView(block.block.classification, input, block.block.annotations) };
|
||||
}
|
||||
|
||||
function messageStreamActivityItemView(
|
||||
activity: Extract<MessageStreamLayoutBlock, { type: "activityGroup" }>["items"][number],
|
||||
input: MessageStreamPresentationBlockInput,
|
||||
): MessageStreamActivityItemView {
|
||||
if (activity.type === "steering") return activity;
|
||||
return { type: "item", id: activity.id, ...messageStreamRenderedItemView(activity.classification, input) };
|
||||
}
|
||||
|
||||
function messageStreamRenderedItemView(
|
||||
classification: MessageStreamSemanticClassification,
|
||||
input: MessageStreamPresentationBlockInput,
|
||||
annotations?: MessageStreamItemAnnotations,
|
||||
): MessageStreamRenderedItemView {
|
||||
const renderFamily = messageStreamRenderFamily(classification);
|
||||
switch (renderFamily) {
|
||||
case "text":
|
||||
return {
|
||||
kind: "text",
|
||||
view: messageStreamTextView(classification.item, annotations, {
|
||||
activeTurnId: input.activeTurnId,
|
||||
...definedProp("actionTargets", input.textActionTargetsByItemId?.get(classification.item.id)),
|
||||
}),
|
||||
};
|
||||
case "detail":
|
||||
return { kind: "detail", view: detailView(classification.item, input.workspaceRoot) };
|
||||
case "status":
|
||||
return { kind: "status", view: messageStreamStatusView(classification.item, statusViewContext(input)) };
|
||||
}
|
||||
}
|
||||
|
||||
function messageStreamRenderFamily(classification: MessageStreamSemanticClassification): MessageStreamRenderFamily {
|
||||
switch (classification.item.kind) {
|
||||
case "message":
|
||||
case "system":
|
||||
case "userInputResult":
|
||||
return "text";
|
||||
case "command":
|
||||
case "fileChange":
|
||||
case "tool":
|
||||
case "hook":
|
||||
case "goal":
|
||||
case "approvalResult":
|
||||
case "reviewResult":
|
||||
case "agent":
|
||||
return "detail";
|
||||
case "taskProgress":
|
||||
case "reasoning":
|
||||
case "wait":
|
||||
case "contextCompaction":
|
||||
return "status";
|
||||
}
|
||||
return "status";
|
||||
}
|
||||
|
||||
function statusViewContext(input: MessageStreamPresentationBlockInput): Parameters<typeof messageStreamStatusView>[1] {
|
||||
return {
|
||||
activeTurnId: input.activeTurnId,
|
||||
items: input.items,
|
||||
activeItems: input.activeItems,
|
||||
};
|
||||
}
|
||||
|
||||
function definedProp<Key extends string, Value>(key: Key, value: Value | undefined): Partial<Record<Key, Value>> {
|
||||
return value === undefined ? {} : ({ [key]: value } as Partial<Record<Key, Value>>);
|
||||
}
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
import { truncate } from "../../../../domain/display/text-preview";
|
||||
import { shortThreadId } from "../../../../domain/threads/id";
|
||||
import { pathRelativeToRoot } from "../../../../domain/vault/paths";
|
||||
import { agentMessagePreview } from "../../domain/message-stream/format/agent-message-preview";
|
||||
import { agentMessagePreview } from "../../domain/thread-stream/format/agent-message-preview";
|
||||
import type {
|
||||
AgentMessageStreamItem,
|
||||
ApprovalResultMessageStreamItem,
|
||||
CommandMessageStreamItem,
|
||||
CommandMessageStreamTarget,
|
||||
AgentThreadStreamItem,
|
||||
ApprovalResultThreadStreamItem,
|
||||
CommandThreadStreamItem,
|
||||
CommandThreadStreamTarget,
|
||||
ExecutionState,
|
||||
FileChangeMessageStreamItem,
|
||||
GoalMessageStreamItem,
|
||||
HookMessageStreamItem,
|
||||
MessageStreamFileChange,
|
||||
MessageStreamItem,
|
||||
MessageStreamPrimaryTarget,
|
||||
ReviewResultMessageStreamItem,
|
||||
ToolCallMessageStreamItem,
|
||||
} from "../../domain/message-stream/items";
|
||||
FileChangeThreadStreamItem,
|
||||
GoalThreadStreamItem,
|
||||
HookThreadStreamItem,
|
||||
ReviewResultThreadStreamItem,
|
||||
ThreadStreamFileChange,
|
||||
ThreadStreamItem,
|
||||
ThreadStreamPrimaryTarget,
|
||||
ToolCallThreadStreamItem,
|
||||
} from "../../domain/thread-stream/items";
|
||||
|
||||
const AGENT_ROW_MESSAGE_PREVIEW_LIMIT = 120;
|
||||
const AGENT_ACTIVITY_PROMPT_PREVIEW_LIMIT = 96;
|
||||
|
|
@ -36,11 +36,11 @@ export interface DetailView {
|
|||
state: ExecutionState;
|
||||
}
|
||||
|
||||
export function detailView(item: MessageStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
export function detailView(item: ThreadStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
return codexDetailView(item, workspaceRoot) ?? genericDetailView(item, workspaceRoot);
|
||||
}
|
||||
|
||||
function codexDetailView(item: MessageStreamItem, workspaceRoot?: string | null): DetailView | null {
|
||||
function codexDetailView(item: ThreadStreamItem, workspaceRoot?: string | null): DetailView | null {
|
||||
switch (item.kind) {
|
||||
case "command":
|
||||
return commandDetailView(item);
|
||||
|
|
@ -63,7 +63,7 @@ function codexDetailView(item: MessageStreamItem, workspaceRoot?: string | null)
|
|||
}
|
||||
|
||||
function detailViewBase(
|
||||
item: MessageStreamItem,
|
||||
item: ThreadStreamItem,
|
||||
className: string,
|
||||
label: string,
|
||||
detailsKey: string,
|
||||
|
|
@ -82,7 +82,7 @@ function detailViewBase(
|
|||
};
|
||||
}
|
||||
|
||||
function commandDetailView(item: CommandMessageStreamItem): DetailView {
|
||||
function commandDetailView(item: CommandThreadStreamItem): DetailView {
|
||||
const rows = [
|
||||
{ key: "command", value: item.command },
|
||||
{ key: "cwd", value: item.cwd },
|
||||
|
|
@ -107,7 +107,7 @@ function commandDetailView(item: CommandMessageStreamItem): DetailView {
|
|||
);
|
||||
}
|
||||
|
||||
function fileChangeDetailView(item: FileChangeMessageStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
function fileChangeDetailView(item: FileChangeThreadStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
const displayChanges = item.changes.map((change) => ({
|
||||
...change,
|
||||
displayPath: change.path && change.path !== "(unknown)" ? pathRelativeToRoot(change.path, workspaceRoot) : change.path,
|
||||
|
|
@ -137,7 +137,7 @@ function fileChangeDetailView(item: FileChangeMessageStreamItem, workspaceRoot?:
|
|||
);
|
||||
}
|
||||
|
||||
function goalDetailView(item: GoalMessageStreamItem): DetailView {
|
||||
function goalDetailView(item: GoalThreadStreamItem): DetailView {
|
||||
return detailViewBase(
|
||||
item,
|
||||
"codex-panel__detail-item codex-panel__detail-item--goal",
|
||||
|
|
@ -147,7 +147,7 @@ function goalDetailView(item: GoalMessageStreamItem): DetailView {
|
|||
);
|
||||
}
|
||||
|
||||
function agentDetailView(item: AgentMessageStreamItem): DetailView {
|
||||
function agentDetailView(item: AgentThreadStreamItem): DetailView {
|
||||
return detailViewBase(
|
||||
item,
|
||||
"codex-panel__detail-item codex-panel__agent-activity",
|
||||
|
|
@ -159,7 +159,7 @@ function agentDetailView(item: AgentMessageStreamItem): DetailView {
|
|||
);
|
||||
}
|
||||
|
||||
function genericToolDetailView(item: ToolCallMessageStreamItem | HookMessageStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
function genericToolDetailView(item: ToolCallThreadStreamItem | HookThreadStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
return detailViewBase(
|
||||
item,
|
||||
"codex-panel__detail-item",
|
||||
|
|
@ -170,7 +170,7 @@ function genericToolDetailView(item: ToolCallMessageStreamItem | HookMessageStre
|
|||
);
|
||||
}
|
||||
|
||||
function reviewDetailView(item: ReviewResultMessageStreamItem): DetailView {
|
||||
function reviewDetailView(item: ReviewResultThreadStreamItem): DetailView {
|
||||
return resultDetailView(
|
||||
item,
|
||||
"auto-review",
|
||||
|
|
@ -179,7 +179,7 @@ function reviewDetailView(item: ReviewResultMessageStreamItem): DetailView {
|
|||
);
|
||||
}
|
||||
|
||||
function approvalDetailView(item: ApprovalResultMessageStreamItem): DetailView {
|
||||
function approvalDetailView(item: ApprovalResultThreadStreamItem): DetailView {
|
||||
return resultDetailView(
|
||||
item,
|
||||
"approval",
|
||||
|
|
@ -188,7 +188,7 @@ function approvalDetailView(item: ApprovalResultMessageStreamItem): DetailView {
|
|||
);
|
||||
}
|
||||
|
||||
function genericDetailView(item: MessageStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
function genericDetailView(item: ThreadStreamItem, workspaceRoot?: string | null): DetailView {
|
||||
return detailViewBase(
|
||||
item,
|
||||
"codex-panel__detail-item",
|
||||
|
|
@ -204,7 +204,7 @@ function messageDetailKey(itemId: string, suffix: string): string {
|
|||
}
|
||||
|
||||
function resultDetailView(
|
||||
item: ApprovalResultMessageStreamItem | ReviewResultMessageStreamItem,
|
||||
item: ApprovalResultThreadStreamItem | ReviewResultThreadStreamItem,
|
||||
label: string,
|
||||
detailsKey: string,
|
||||
className: string,
|
||||
|
|
@ -212,7 +212,7 @@ function resultDetailView(
|
|||
return detailViewBase(item, `codex-panel__detail-item ${className}`, label, detailsKey, resultDetails(item));
|
||||
}
|
||||
|
||||
function goalDetails(item: GoalMessageStreamItem): DetailSection[] {
|
||||
function goalDetails(item: GoalThreadStreamItem): DetailSection[] {
|
||||
return [
|
||||
{
|
||||
kind: "kv",
|
||||
|
|
@ -222,7 +222,7 @@ function goalDetails(item: GoalMessageStreamItem): DetailSection[] {
|
|||
];
|
||||
}
|
||||
|
||||
function agentDetailSections(item: AgentMessageStreamItem): DetailSection[] {
|
||||
function agentDetailSections(item: AgentThreadStreamItem): DetailSection[] {
|
||||
const rows = [
|
||||
{ key: "tool", value: agentActivityMetaLabel(item.tool) },
|
||||
{ key: "status", value: item.status },
|
||||
|
|
@ -243,7 +243,7 @@ function agentDetailSections(item: AgentMessageStreamItem): DetailSection[] {
|
|||
];
|
||||
}
|
||||
|
||||
function agentStateSection(item: AgentMessageStreamItem): DetailSection[] {
|
||||
function agentStateSection(item: AgentThreadStreamItem): DetailSection[] {
|
||||
const rows = item.agents.map((agent) => ({
|
||||
key: shortThreadId(agent.threadId),
|
||||
value: agentStatusLabel(agent.status, agent.message),
|
||||
|
|
@ -251,7 +251,7 @@ function agentStateSection(item: AgentMessageStreamItem): DetailSection[] {
|
|||
return rows.length > 0 ? [{ kind: "kv", title: "agents", rows }] : [];
|
||||
}
|
||||
|
||||
function resultDetails(item: ApprovalResultMessageStreamItem | ReviewResultMessageStreamItem): DetailSection[] {
|
||||
function resultDetails(item: ApprovalResultThreadStreamItem | ReviewResultThreadStreamItem): DetailSection[] {
|
||||
if (item.kind === "approvalResult") {
|
||||
return [
|
||||
{
|
||||
|
|
@ -268,12 +268,12 @@ function resultDetails(item: ApprovalResultMessageStreamItem | ReviewResultMessa
|
|||
return item.review?.auditFacts && item.review.auditFacts.length > 0 ? [{ kind: "kv", rows: item.review.auditFacts }] : [];
|
||||
}
|
||||
|
||||
function genericToolDetails(item: ToolCallMessageStreamItem | HookMessageStreamItem): DetailSection[] {
|
||||
function genericToolDetails(item: ToolCallThreadStreamItem | HookThreadStreamItem): DetailSection[] {
|
||||
if (item.kind === "hook") return hookRunDetails(item);
|
||||
return [...diagnosticDetails(item), ...webSearchDetails(item), ...imageGenerationDetails(item)];
|
||||
}
|
||||
|
||||
function genericDetailSections(item: MessageStreamItem, workspaceRoot?: string | null): DetailSection[] {
|
||||
function genericDetailSections(item: ThreadStreamItem, workspaceRoot?: string | null): DetailSection[] {
|
||||
const rows = [
|
||||
...metaRow("kind", item.kind),
|
||||
...metaRow("status", stringField(item, "status")),
|
||||
|
|
@ -284,11 +284,11 @@ function genericDetailSections(item: MessageStreamItem, workspaceRoot?: string |
|
|||
return [...(rows.length > 0 ? [{ kind: "kv" as const, rows }] : []), ...outputSection("Output", outputField(item))];
|
||||
}
|
||||
|
||||
function diagnosticDetails(item: ToolCallMessageStreamItem): DetailSection[] {
|
||||
function diagnosticDetails(item: ToolCallThreadStreamItem): DetailSection[] {
|
||||
return item.diagnostics?.map((section) => ({ kind: "output" as const, title: section.title, body: section.body })) ?? [];
|
||||
}
|
||||
|
||||
function webSearchDetails(item: ToolCallMessageStreamItem): DetailSection[] {
|
||||
function webSearchDetails(item: ToolCallThreadStreamItem): DetailSection[] {
|
||||
const details = item.webSearch;
|
||||
if (!details) return [];
|
||||
const rows = [
|
||||
|
|
@ -300,7 +300,7 @@ function webSearchDetails(item: ToolCallMessageStreamItem): DetailSection[] {
|
|||
return rows.length > 0 ? [{ kind: "kv", title: "web search", rows }] : [];
|
||||
}
|
||||
|
||||
function imageGenerationDetails(item: ToolCallMessageStreamItem): DetailSection[] {
|
||||
function imageGenerationDetails(item: ToolCallThreadStreamItem): DetailSection[] {
|
||||
const details = item.imageGeneration;
|
||||
if (!details) return [];
|
||||
return [
|
||||
|
|
@ -310,7 +310,7 @@ function imageGenerationDetails(item: ToolCallMessageStreamItem): DetailSection[
|
|||
];
|
||||
}
|
||||
|
||||
function hookRunDetails(item: HookMessageStreamItem): DetailSection[] {
|
||||
function hookRunDetails(item: HookThreadStreamItem): DetailSection[] {
|
||||
const details = item.hookRun;
|
||||
if (!details) return [];
|
||||
const rows = [
|
||||
|
|
@ -331,21 +331,21 @@ function metaRow(key: string, value: string | null | undefined): { key: string;
|
|||
return value ? [{ key, value }] : [];
|
||||
}
|
||||
|
||||
function primaryTargetSummary(target: MessageStreamPrimaryTarget | undefined, workspaceRoot?: string | null): string | null {
|
||||
function primaryTargetSummary(target: ThreadStreamPrimaryTarget | undefined, workspaceRoot?: string | null): string | null {
|
||||
if (!target) return null;
|
||||
if (target.kind === "path") return pathRelativeToRoot(target.path, workspaceRoot);
|
||||
return target.value;
|
||||
}
|
||||
|
||||
function textField(item: MessageStreamItem): string | null {
|
||||
function textField(item: ThreadStreamItem): string | null {
|
||||
return "text" in item && typeof item.text === "string" && item.text.trim().length > 0 ? item.text : null;
|
||||
}
|
||||
|
||||
function outputField(item: MessageStreamItem): string | null {
|
||||
function outputField(item: ThreadStreamItem): string | null {
|
||||
return "output" in item && typeof item.output === "string" && item.output.trim().length > 0 ? item.output : null;
|
||||
}
|
||||
|
||||
function stringField(item: MessageStreamItem, key: "failureReason" | "operation" | "status" | "toolName"): string | null {
|
||||
function stringField(item: ThreadStreamItem, key: "failureReason" | "operation" | "status" | "toolName"): string | null {
|
||||
if (!(key in item)) return null;
|
||||
const value = (item as unknown as Record<string, unknown>)[key];
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
|
|
@ -363,22 +363,22 @@ function statusQualifier(status: unknown, failure?: string | null): string | nul
|
|||
return null;
|
||||
}
|
||||
|
||||
function fallbackSummary(item: MessageStreamItem): string {
|
||||
function fallbackSummary(item: ThreadStreamItem): string {
|
||||
return textField(item) ?? "details";
|
||||
}
|
||||
|
||||
function commandActionLabel(action: CommandMessageStreamItem["commandAction"]): string {
|
||||
function commandActionLabel(action: CommandThreadStreamItem["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 {
|
||||
function commandSummary(item: CommandThreadStreamItem): string {
|
||||
return compactSummary(null, commandTargetSummary(item.commandTarget, item.cwd), commandQualifier(item));
|
||||
}
|
||||
|
||||
function commandTargetSummary(target: CommandMessageStreamTarget, cwd: string): string {
|
||||
function commandTargetSummary(target: CommandThreadStreamTarget, 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;
|
||||
|
|
@ -392,32 +392,32 @@ function commandTargetSummary(target: CommandMessageStreamTarget, cwd: string):
|
|||
return target.commandLine;
|
||||
}
|
||||
|
||||
function commandQualifier(item: CommandMessageStreamItem): string | null {
|
||||
function commandQualifier(item: CommandThreadStreamItem): string | null {
|
||||
if (typeof item.exitCode === "number" && item.exitCode !== 0) return `exit ${String(item.exitCode)}`;
|
||||
return statusQualifier(item.status, failedStatusLabel(item.status));
|
||||
}
|
||||
|
||||
function genericDetailSummary(item: MessageStreamItem, workspaceRoot?: string | null): string {
|
||||
function genericDetailSummary(item: ThreadStreamItem, workspaceRoot?: string | null): string {
|
||||
const target = primaryTargetSummary(primaryTargetField(item), workspaceRoot);
|
||||
return compactSummary(null, target ?? textField(item) ?? outputField(item) ?? stringField(item, "status") ?? item.kind);
|
||||
}
|
||||
|
||||
function detailLabel(item: MessageStreamItem): string {
|
||||
function detailLabel(item: ThreadStreamItem): string {
|
||||
return stringField(item, "toolName") ?? item.kind;
|
||||
}
|
||||
|
||||
function primaryTargetField(item: MessageStreamItem): MessageStreamPrimaryTarget | undefined {
|
||||
function primaryTargetField(item: ThreadStreamItem): ThreadStreamPrimaryTarget | undefined {
|
||||
if (!("primaryTarget" in item)) return undefined;
|
||||
return item.primaryTarget;
|
||||
}
|
||||
|
||||
function genericToolSummary(item: ToolCallMessageStreamItem | HookMessageStreamItem, workspaceRoot?: string | null): string {
|
||||
function genericToolSummary(item: ToolCallThreadStreamItem | HookThreadStreamItem, 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: FileChangeMessageStreamItem, changes: (MessageStreamFileChange & { displayPath: string })[]): string {
|
||||
function fileChangeSummary(item: FileChangeThreadStreamItem, changes: (ThreadStreamFileChange & { displayPath: string })[]): string {
|
||||
const target = fileChangeTargetSummary(changes);
|
||||
return compactSummary(null, target, statusQualifier(item.status, failedStatusLabel(item.status)));
|
||||
}
|
||||
|
|
@ -428,13 +428,13 @@ function failedStatusLabel(status: unknown): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function agentSummaryText(item: AgentMessageStreamItem): string {
|
||||
function agentSummaryText(item: AgentThreadStreamItem): string {
|
||||
const target = item.receiverThreadIds.length === 0 ? "" : ` ${item.receiverThreadIds.map(shortThreadId).join(", ")}`;
|
||||
const promptPreview = agentPromptPreview(item.prompt);
|
||||
return `${agentActivityMetaLabel(item.tool)}${target}${promptPreview ? `: ${promptPreview}` : ""} (${item.status})`;
|
||||
}
|
||||
|
||||
function agentThreadIds(item: AgentMessageStreamItem): readonly string[] {
|
||||
function agentThreadIds(item: AgentThreadStreamItem): readonly string[] {
|
||||
return [...new Set([...item.receiverThreadIds, ...item.agents.map((agent) => agent.threadId)])].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
|
|
@ -462,7 +462,7 @@ function isLongAgentMessage(message: string): boolean {
|
|||
return message.length > AGENT_ROW_MESSAGE_PREVIEW_LIMIT || message.includes("\n");
|
||||
}
|
||||
|
||||
function fileChangeTargetSummary(changes: (MessageStreamFileChange & { displayPath: string })[]): string {
|
||||
function fileChangeTargetSummary(changes: (ThreadStreamFileChange & { 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`;
|
||||
|
|
@ -1,28 +1,28 @@
|
|||
import { pathRelativeToRoot } from "../../../../domain/vault/paths";
|
||||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamSemanticClassifications } from "../../domain/message-stream/semantics/classify";
|
||||
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { threadStreamSemanticClassifications } from "../../domain/thread-stream/semantics/classify";
|
||||
import {
|
||||
messageStreamIsAutoReviewDecision,
|
||||
messageStreamIsTurnInitiator,
|
||||
messageStreamIsTurnSteer,
|
||||
messageStreamIsWorkspaceResult,
|
||||
} from "../../domain/message-stream/semantics/predicates";
|
||||
import type { MessageStreamSemanticClassification } from "../../domain/message-stream/semantics/types";
|
||||
threadStreamIsAutoReviewDecision,
|
||||
threadStreamIsTurnInitiator,
|
||||
threadStreamIsTurnSteer,
|
||||
threadStreamIsWorkspaceResult,
|
||||
} from "../../domain/thread-stream/semantics/predicates";
|
||||
import type { ThreadStreamSemanticClassification } from "../../domain/thread-stream/semantics/types";
|
||||
|
||||
const STEERING_ACTIVITY_LABEL = "steer";
|
||||
|
||||
export interface MessageStreamItemAnnotations {
|
||||
export interface ThreadStreamItemAnnotations {
|
||||
editedFiles?: string[];
|
||||
turnDiff?: { diff: string };
|
||||
autoReviewSummaries?: string[];
|
||||
}
|
||||
|
||||
type MessageStreamActivityGroupItem =
|
||||
type ThreadStreamActivityGroupItem =
|
||||
| {
|
||||
type: "item";
|
||||
id: string;
|
||||
item: MessageStreamItem;
|
||||
classification: MessageStreamSemanticClassification;
|
||||
item: ThreadStreamItem;
|
||||
classification: ThreadStreamSemanticClassification;
|
||||
}
|
||||
| {
|
||||
type: "steering";
|
||||
|
|
@ -32,28 +32,28 @@ type MessageStreamActivityGroupItem =
|
|||
sourceItemId: string;
|
||||
};
|
||||
|
||||
export type MessageStreamLayoutBlock =
|
||||
export type ThreadStreamLayoutBlock =
|
||||
| {
|
||||
type: "item";
|
||||
item: MessageStreamItem;
|
||||
classification: MessageStreamSemanticClassification;
|
||||
annotations?: MessageStreamItemAnnotations;
|
||||
item: ThreadStreamItem;
|
||||
classification: ThreadStreamSemanticClassification;
|
||||
annotations?: ThreadStreamItemAnnotations;
|
||||
}
|
||||
| {
|
||||
type: "activityGroup";
|
||||
id: string;
|
||||
turnId: string;
|
||||
summary: string;
|
||||
items: MessageStreamActivityGroupItem[];
|
||||
items: ThreadStreamActivityGroupItem[];
|
||||
};
|
||||
|
||||
export function messageStreamLayoutBlocks(
|
||||
items: readonly MessageStreamItem[],
|
||||
export function threadStreamLayoutBlocks(
|
||||
items: readonly ThreadStreamItem[],
|
||||
activeTurnId: string | null,
|
||||
workspaceRoot?: string | null,
|
||||
turnDiffs?: ReadonlyMap<string, string>,
|
||||
): MessageStreamLayoutBlock[] {
|
||||
const visibleItems = messageStreamSemanticClassifications(items).filter(shouldShowPresentationItem);
|
||||
): ThreadStreamLayoutBlock[] {
|
||||
const visibleItems = threadStreamSemanticClassifications(items).filter(shouldShowPresentationItem);
|
||||
const editedFilesByTurn = editedFilesForTurns(visibleItems, workspaceRoot);
|
||||
const autoReviewSummariesByTurn = autoReviewSummariesForTurns(visibleItems);
|
||||
const turnOutcomeIdByTurn = turnOutcomeItemsByTurn(visibleItems);
|
||||
|
|
@ -65,7 +65,7 @@ export function messageStreamLayoutBlocks(
|
|||
const { item } = classification;
|
||||
const turnId = item.turnId;
|
||||
if (!turnId || !groupedTurnIds.has(turnId)) continue;
|
||||
if (messageStreamIsTurnSteer(classification) && item.kind === "message") {
|
||||
if (threadStreamIsTurnSteer(classification) && item.kind === "dialogue") {
|
||||
const group = groupedActivities.get(turnId) ?? [];
|
||||
group.push(steeringActivityGroupItem(classification));
|
||||
groupedActivities.set(turnId, group);
|
||||
|
|
@ -77,7 +77,7 @@ export function messageStreamLayoutBlocks(
|
|||
groupedActivities.set(turnId, group);
|
||||
}
|
||||
|
||||
const blocks: MessageStreamLayoutBlock[] = [];
|
||||
const blocks: ThreadStreamLayoutBlock[] = [];
|
||||
for (const classification of visibleItems) {
|
||||
const { item } = classification;
|
||||
const turnId = item.turnId;
|
||||
|
|
@ -108,22 +108,22 @@ export function messageStreamLayoutBlocks(
|
|||
return blocks;
|
||||
}
|
||||
|
||||
type GroupedActivity = MessageStreamActivityGroupItem;
|
||||
type GroupedActivity = ThreadStreamActivityGroupItem;
|
||||
|
||||
function shouldShowPresentationItem(classification: MessageStreamSemanticClassification): boolean {
|
||||
function shouldShowPresentationItem(classification: ThreadStreamSemanticClassification): boolean {
|
||||
return !isEmptyCompletedReasoningItem(classification.item);
|
||||
}
|
||||
|
||||
function isEmptyCompletedReasoningItem(item: MessageStreamItem): boolean {
|
||||
return item.kind === "reasoning" && item.executionState === "completed" && textForMessageStreamItem(item).trim().length === 0;
|
||||
function isEmptyCompletedReasoningItem(item: ThreadStreamItem): boolean {
|
||||
return item.kind === "reasoning" && item.executionState === "completed" && textForThreadStreamItem(item).trim().length === 0;
|
||||
}
|
||||
|
||||
function steeringActivityGroupItem(classification: MessageStreamSemanticClassification): MessageStreamActivityGroupItem {
|
||||
function steeringActivityGroupItem(classification: ThreadStreamSemanticClassification): ThreadStreamActivityGroupItem {
|
||||
return {
|
||||
type: "steering",
|
||||
id: steerActivityGroupId(classification.item.id),
|
||||
label: STEERING_ACTIVITY_LABEL,
|
||||
text: textForMessageStreamItem(classification.item),
|
||||
text: textForThreadStreamItem(classification.item),
|
||||
sourceItemId: classification.item.sourceItemId ?? classification.item.id,
|
||||
};
|
||||
}
|
||||
|
|
@ -136,13 +136,13 @@ function steerActivityGroupId(itemId: string): string {
|
|||
return `steer-activity-${itemId}`;
|
||||
}
|
||||
|
||||
function isCompletedTurnDetailItem(classification: MessageStreamSemanticClassification, turnOutcomeIdByTurn: Map<string, string>): boolean {
|
||||
function isCompletedTurnDetailItem(classification: ThreadStreamSemanticClassification, turnOutcomeIdByTurn: Map<string, string>): boolean {
|
||||
const turnId = classification.item.turnId;
|
||||
if (!turnId || messageStreamIsTurnInitiator(classification) || messageStreamIsTurnSteer(classification)) return false;
|
||||
if (!turnId || threadStreamIsTurnInitiator(classification) || threadStreamIsTurnSteer(classification)) return false;
|
||||
return turnOutcomeIdByTurn.get(turnId) !== classification.item.id;
|
||||
}
|
||||
|
||||
function turnOutcomeItemsByTurn(items: readonly MessageStreamSemanticClassification[]): Map<string, string> {
|
||||
function turnOutcomeItemsByTurn(items: readonly ThreadStreamSemanticClassification[]): Map<string, string> {
|
||||
const turnOutcomeIdByTurn = new Map<string, string>();
|
||||
for (const { item, capabilities } of items) {
|
||||
if (!item.turnId || !capabilities.isTurnOutcome) continue;
|
||||
|
|
@ -152,14 +152,14 @@ function turnOutcomeItemsByTurn(items: readonly MessageStreamSemanticClassificat
|
|||
}
|
||||
|
||||
function annotationsForTurnOutcome(
|
||||
item: MessageStreamItem,
|
||||
item: ThreadStreamItem,
|
||||
editedFilesByTurn: Map<string, string[]>,
|
||||
autoReviewSummariesByTurn: Map<string, string[]>,
|
||||
turnOutcomeIdByTurn: Map<string, string>,
|
||||
turnDiffs?: ReadonlyMap<string, string>,
|
||||
): MessageStreamItemAnnotations | undefined {
|
||||
): ThreadStreamItemAnnotations | undefined {
|
||||
if (!item.turnId || turnOutcomeIdByTurn.get(item.turnId) !== item.id) return undefined;
|
||||
if (item.kind !== "message") return undefined;
|
||||
if (item.kind !== "dialogue") return undefined;
|
||||
const editedFiles = editedFilesByTurn.get(item.turnId);
|
||||
const autoReviewSummaries = autoReviewSummariesByTurn.get(item.turnId);
|
||||
const diff = turnDiffs?.get(item.turnId);
|
||||
|
|
@ -174,11 +174,11 @@ function annotationsForTurnOutcome(
|
|||
};
|
||||
}
|
||||
|
||||
function editedFilesForTurns(items: readonly MessageStreamSemanticClassification[], workspaceRoot?: string | null): Map<string, string[]> {
|
||||
function editedFilesForTurns(items: readonly ThreadStreamSemanticClassification[], workspaceRoot?: string | null): Map<string, string[]> {
|
||||
const byTurn = new Map<string, Set<string>>();
|
||||
for (const classification of items) {
|
||||
const { item } = classification;
|
||||
if (!item.turnId || !messageStreamIsWorkspaceResult(classification)) continue;
|
||||
if (!item.turnId || !threadStreamIsWorkspaceResult(classification)) continue;
|
||||
const files = editedFilesForItem(item, workspaceRoot);
|
||||
if (files.length === 0) continue;
|
||||
const set = byTurn.get(item.turnId) ?? new Set<string>();
|
||||
|
|
@ -189,21 +189,21 @@ function editedFilesForTurns(items: readonly MessageStreamSemanticClassification
|
|||
return new Map([...byTurn].map(([turnId, files]) => [turnId, [...files].sort((a, b) => a.localeCompare(b))]));
|
||||
}
|
||||
|
||||
function editedFilesForItem(item: MessageStreamItem, workspaceRoot?: string | null): string[] {
|
||||
function editedFilesForItem(item: ThreadStreamItem, workspaceRoot?: string | null): string[] {
|
||||
if (item.kind !== "fileChange") return [];
|
||||
return item.changes.flatMap((change) =>
|
||||
change.path && change.path !== "(unknown)" ? [pathRelativeToRoot(change.path, workspaceRoot)] : [],
|
||||
);
|
||||
}
|
||||
|
||||
function autoReviewSummariesForTurns(items: readonly MessageStreamSemanticClassification[]): Map<string, string[]> {
|
||||
function autoReviewSummariesForTurns(items: readonly ThreadStreamSemanticClassification[]): Map<string, string[]> {
|
||||
const byTurn = new Map<string, string[]>();
|
||||
for (const classification of items) {
|
||||
const { item } = classification;
|
||||
if (!item.turnId || !messageStreamIsAutoReviewDecision(classification)) {
|
||||
if (!item.turnId || !threadStreamIsAutoReviewDecision(classification)) {
|
||||
continue;
|
||||
}
|
||||
const summary = textForMessageStreamItem(item).trim();
|
||||
const summary = textForThreadStreamItem(item).trim();
|
||||
if (!summary) continue;
|
||||
const summaries = byTurn.get(item.turnId) ?? [];
|
||||
summaries.push(summary);
|
||||
|
|
@ -212,7 +212,7 @@ function autoReviewSummariesForTurns(items: readonly MessageStreamSemanticClassi
|
|||
return byTurn;
|
||||
}
|
||||
|
||||
function textForMessageStreamItem(item: MessageStreamItem): string {
|
||||
function textForThreadStreamItem(item: ThreadStreamItem): string {
|
||||
return "text" in item && typeof item.text === "string" ? item.text : "";
|
||||
}
|
||||
|
||||
|
|
@ -3,15 +3,15 @@ import type {
|
|||
AgentRunSummary,
|
||||
AgentRunSummaryAgent,
|
||||
ExecutionState,
|
||||
MessageStreamItem,
|
||||
ReasoningMessageStreamItem,
|
||||
TaskProgressMessageStreamItem,
|
||||
} from "../../domain/message-stream/items";
|
||||
import { messageStreamReasoningIsActive } from "../../domain/message-stream/semantics/active-turn";
|
||||
ReasoningThreadStreamItem,
|
||||
TaskProgressThreadStreamItem,
|
||||
ThreadStreamItem,
|
||||
} from "../../domain/thread-stream/items";
|
||||
import { threadStreamReasoningIsActive } from "../../domain/thread-stream/semantics/active-turn";
|
||||
|
||||
type StatusChecklistItem = TaskProgressMessageStreamItem["steps"][number];
|
||||
type StatusChecklistItem = TaskProgressThreadStreamItem["steps"][number];
|
||||
|
||||
export type MessageStreamStatusView =
|
||||
export type ThreadStreamStatusView =
|
||||
| {
|
||||
kind: "taskProgress";
|
||||
label: "tasks";
|
||||
|
|
@ -41,10 +41,10 @@ export type MessageStreamStatusView =
|
|||
text: string;
|
||||
};
|
||||
|
||||
export interface MessageStreamStatusViewContext {
|
||||
export interface ThreadStreamStatusViewContext {
|
||||
activeTurnId: string | null;
|
||||
items: readonly MessageStreamItem[];
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
items: readonly ThreadStreamItem[];
|
||||
activeItems?: readonly ThreadStreamItem[] | undefined;
|
||||
}
|
||||
|
||||
export interface AgentRunSummaryView {
|
||||
|
|
@ -56,7 +56,7 @@ export interface AgentRunSummaryView {
|
|||
additionalAgents: number;
|
||||
}
|
||||
|
||||
export function messageStreamStatusView(item: MessageStreamItem, context: MessageStreamStatusViewContext): MessageStreamStatusView {
|
||||
export function threadStreamStatusView(item: ThreadStreamItem, context: ThreadStreamStatusViewContext): ThreadStreamStatusView {
|
||||
if (item.kind === "taskProgress") {
|
||||
return {
|
||||
kind: "taskProgress",
|
||||
|
|
@ -83,7 +83,7 @@ export function agentRunSummaryView(summary: AgentRunSummary): AgentRunSummaryVi
|
|||
};
|
||||
}
|
||||
|
||||
function contextCompactionStatusView(item: MessageStreamItem, context: MessageStreamStatusViewContext): MessageStreamStatusView {
|
||||
function contextCompactionStatusView(item: ThreadStreamItem, context: ThreadStreamStatusViewContext): ThreadStreamStatusView {
|
||||
const active = context.activeTurnId === item.turnId;
|
||||
return {
|
||||
kind: "contextCompaction",
|
||||
|
|
@ -94,8 +94,8 @@ function contextCompactionStatusView(item: MessageStreamItem, context: MessageSt
|
|||
};
|
||||
}
|
||||
|
||||
function reasoningStatusView(item: ReasoningMessageStreamItem, context: MessageStreamStatusViewContext): MessageStreamStatusView {
|
||||
const active = messageStreamReasoningIsActive(item, context);
|
||||
function reasoningStatusView(item: ReasoningThreadStreamItem, context: ThreadStreamStatusViewContext): ThreadStreamStatusView {
|
||||
const active = threadStreamReasoningIsActive(item, context);
|
||||
return {
|
||||
kind: "reasoning",
|
||||
active,
|
||||
|
|
@ -104,7 +104,7 @@ function reasoningStatusView(item: ReasoningMessageStreamItem, context: MessageS
|
|||
};
|
||||
}
|
||||
|
||||
function genericStatusView(item: MessageStreamItem): MessageStreamStatusView {
|
||||
function genericStatusView(item: ThreadStreamItem): ThreadStreamStatusView {
|
||||
return {
|
||||
kind: "generic",
|
||||
label: item.kind,
|
||||
|
|
@ -136,7 +136,7 @@ function agentRunSummaryRow(agent: AgentRunSummaryAgent): { threadId: string; th
|
|||
};
|
||||
}
|
||||
|
||||
function stringField(item: MessageStreamItem, key: "failureReason" | "operation" | "output" | "status" | "text"): string | null {
|
||||
function stringField(item: ThreadStreamItem, key: "failureReason" | "operation" | "output" | "status" | "text"): string | null {
|
||||
if (!(key in item)) return null;
|
||||
const value = (item as unknown as Record<string, unknown>)[key];
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
import type {
|
||||
ExecutionState,
|
||||
MessageStreamItem,
|
||||
MessageStreamNoticeSection,
|
||||
MessageStreamUserInputQuestionResult,
|
||||
} from "../../domain/message-stream/items";
|
||||
import type { PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import type { MessageStreamItemAnnotations } from "./layout";
|
||||
ThreadStreamItem,
|
||||
ThreadStreamNoticeSection,
|
||||
ThreadStreamUserInputQuestionResult,
|
||||
} from "../../domain/thread-stream/items";
|
||||
import type { PlanImplementationTarget } from "../../domain/thread-stream/selectors";
|
||||
import type { ThreadStreamItemAnnotations } from "./layout";
|
||||
|
||||
export interface MessageStreamForkTarget {
|
||||
export interface ThreadStreamForkTarget {
|
||||
itemId: string;
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamTextActionTargets {
|
||||
fork?: MessageStreamForkTarget;
|
||||
export interface ThreadStreamTextActionTargets {
|
||||
fork?: ThreadStreamForkTarget;
|
||||
rollback?: true;
|
||||
implementPlan?: PlanImplementationTarget;
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ export interface TextItemDetailSectionView {
|
|||
body?: string;
|
||||
}
|
||||
|
||||
interface MessageStreamTextMetadataView {
|
||||
interface ThreadStreamTextMetadataView {
|
||||
editedFiles?: EditedFilesTextView;
|
||||
referencedThread?: ReferencedThreadTextView;
|
||||
mentionedFiles?: {
|
||||
|
|
@ -55,26 +55,26 @@ interface MessageStreamTextMetadataView {
|
|||
userInputDetails: readonly TextItemDetailSectionView[];
|
||||
}
|
||||
|
||||
export interface MessageStreamTextView {
|
||||
export interface ThreadStreamTextView {
|
||||
id: string;
|
||||
roleLabel: string;
|
||||
body: string;
|
||||
className: string;
|
||||
contentKey: string;
|
||||
renderMode: MessageStreamTextRenderMode;
|
||||
renderMode: ThreadStreamTextRenderMode;
|
||||
collapsible: boolean;
|
||||
copyText?: string;
|
||||
actionTargets: MessageStreamTextActionTargets;
|
||||
metadata: MessageStreamTextMetadataView;
|
||||
actionTargets: ThreadStreamTextActionTargets;
|
||||
metadata: ThreadStreamTextMetadataView;
|
||||
}
|
||||
|
||||
type MessageStreamTextRenderMode = "text" | "streamMarkdown" | "obsidianMarkdown";
|
||||
type ThreadStreamTextRenderMode = "text" | "streamMarkdown" | "obsidianMarkdown";
|
||||
|
||||
export function messageStreamTextView(
|
||||
item: MessageStreamItem,
|
||||
annotations?: MessageStreamItemAnnotations,
|
||||
options: { activeTurnId?: string | null; actionTargets?: MessageStreamTextActionTargets } = {},
|
||||
): MessageStreamTextView {
|
||||
export function threadStreamTextView(
|
||||
item: ThreadStreamItem,
|
||||
annotations?: ThreadStreamItemAnnotations,
|
||||
options: { activeTurnId?: string | null; actionTargets?: ThreadStreamTextActionTargets } = {},
|
||||
): ThreadStreamTextView {
|
||||
const renderMode = textRenderMode(item);
|
||||
const body = bodyForTextItem(item);
|
||||
return {
|
||||
|
|
@ -84,66 +84,66 @@ export function messageStreamTextView(
|
|||
className: `${textItemClass(item)}${executionClassName(item.executionState ?? null)}`,
|
||||
contentKey: `${item.id}\u001f${renderMode}`,
|
||||
renderMode,
|
||||
collapsible: item.kind === "message" && item.role === "user",
|
||||
collapsible: item.kind === "dialogue" && item.role === "user",
|
||||
...definedProp("copyText", copyTextForTextItem(item, options.activeTurnId ?? null)),
|
||||
actionTargets: options.actionTargets ?? {},
|
||||
metadata: textMetadataView(item, annotations),
|
||||
};
|
||||
}
|
||||
|
||||
function textRenderMode(item: MessageStreamItem): MessageStreamTextRenderMode {
|
||||
if (item.kind !== "message") return "text";
|
||||
if (item.messageKind === "assistantResponse" && item.messageState === "streaming") return "streamMarkdown";
|
||||
return item.messageKind !== "proposedPlan" || item.messageState === "completed" ? "obsidianMarkdown" : "text";
|
||||
function textRenderMode(item: ThreadStreamItem): ThreadStreamTextRenderMode {
|
||||
if (item.kind !== "dialogue") return "text";
|
||||
if (item.dialogueKind === "assistantResponse" && item.dialogueState === "streaming") return "streamMarkdown";
|
||||
return item.dialogueKind !== "proposedPlan" || item.dialogueState === "completed" ? "obsidianMarkdown" : "text";
|
||||
}
|
||||
|
||||
function bodyForTextItem(item: MessageStreamItem): string {
|
||||
function bodyForTextItem(item: ThreadStreamItem): string {
|
||||
return "text" in item && typeof item.text === "string" ? item.text : "";
|
||||
}
|
||||
|
||||
function roleLabelForTextItem(item: MessageStreamItem): string {
|
||||
function roleLabelForTextItem(item: ThreadStreamItem): string {
|
||||
if (item.kind === "userInputResult") return "Input";
|
||||
if (item.role === "user") return "You";
|
||||
if (item.role === "assistant") return "Codex";
|
||||
return "System";
|
||||
}
|
||||
|
||||
function copyTextForTextItem(item: MessageStreamItem, activeTurnId: string | null): string | undefined {
|
||||
if (item.kind !== "message" || item.copyText === undefined) return undefined;
|
||||
function copyTextForTextItem(item: ThreadStreamItem, activeTurnId: string | null): string | undefined {
|
||||
if (item.kind !== "dialogue" || item.copyText === undefined) return undefined;
|
||||
if (activeTurnId && item.role === "assistant" && item.turnId === activeTurnId) return undefined;
|
||||
return item.copyText;
|
||||
}
|
||||
|
||||
function textMetadataView(item: MessageStreamItem, annotations?: MessageStreamItemAnnotations): MessageStreamTextMetadataView {
|
||||
function textMetadataView(item: ThreadStreamItem, annotations?: ThreadStreamItemAnnotations): ThreadStreamTextMetadataView {
|
||||
return {
|
||||
...definedProp("editedFiles", editedFilesView(item, annotations)),
|
||||
...definedProp("referencedThread", referencedThreadView(item)),
|
||||
...definedProp("mentionedFiles", mentionedFilesView(item)),
|
||||
autoReviewSummaries: item.kind === "message" ? (annotations?.autoReviewSummaries ?? []) : [],
|
||||
autoReviewSummaries: item.kind === "dialogue" ? (annotations?.autoReviewSummaries ?? []) : [],
|
||||
systemDetails: item.kind === "system" ? systemDetailViews(item.noticeSections ?? []) : [],
|
||||
userInputDetails: item.kind === "userInputResult" ? userInputQuestionDetailViews(item.questions) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function editedFilesView(item: MessageStreamItem, annotations?: MessageStreamItemAnnotations): EditedFilesTextView | undefined {
|
||||
if (item.kind !== "message" || !annotations?.editedFiles || annotations.editedFiles.length === 0) return undefined;
|
||||
function editedFilesView(item: ThreadStreamItem, annotations?: ThreadStreamItemAnnotations): EditedFilesTextView | undefined {
|
||||
if (item.kind !== "dialogue" || !annotations?.editedFiles || annotations.editedFiles.length === 0) return undefined;
|
||||
return {
|
||||
files: annotations.editedFiles,
|
||||
...definedProp("turnDiff", item.turnId && annotations.turnDiff ? { turnId: item.turnId, diff: annotations.turnDiff.diff } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function referencedThreadView(item: MessageStreamItem): ReferencedThreadTextView | undefined {
|
||||
if (item.kind !== "message" || !item.referencedThread) return undefined;
|
||||
function referencedThreadView(item: ThreadStreamItem): ReferencedThreadTextView | undefined {
|
||||
if (item.kind !== "dialogue" || !item.referencedThread) return undefined;
|
||||
return item.referencedThread;
|
||||
}
|
||||
|
||||
function mentionedFilesView(item: MessageStreamItem): MessageStreamTextMetadataView["mentionedFiles"] | undefined {
|
||||
if (item.kind !== "message" || !item.mentionedFiles || item.mentionedFiles.length === 0) return undefined;
|
||||
function mentionedFilesView(item: ThreadStreamItem): ThreadStreamTextMetadataView["mentionedFiles"] | undefined {
|
||||
if (item.kind !== "dialogue" || !item.mentionedFiles || item.mentionedFiles.length === 0) return undefined;
|
||||
return { itemId: item.id, files: item.mentionedFiles };
|
||||
}
|
||||
|
||||
function systemDetailViews(sections: readonly MessageStreamNoticeSection[]): readonly TextItemDetailSectionView[] {
|
||||
function systemDetailViews(sections: readonly ThreadStreamNoticeSection[]): readonly TextItemDetailSectionView[] {
|
||||
return sections.map((section) => ({
|
||||
...(section.title !== undefined ? { title: section.title } : {}),
|
||||
...(section.auditFacts !== undefined ? { facts: section.auditFacts } : {}),
|
||||
|
|
@ -151,7 +151,7 @@ function systemDetailViews(sections: readonly MessageStreamNoticeSection[]): rea
|
|||
}));
|
||||
}
|
||||
|
||||
function userInputQuestionDetailViews(questions: readonly MessageStreamUserInputQuestionResult[]): readonly TextItemDetailSectionView[] {
|
||||
function userInputQuestionDetailViews(questions: readonly ThreadStreamUserInputQuestionResult[]): readonly TextItemDetailSectionView[] {
|
||||
return questions.map((question) => ({
|
||||
title: `Question: ${question.header}`,
|
||||
facts: [
|
||||
|
|
@ -168,7 +168,7 @@ function executionClassName(state: ExecutionState): string {
|
|||
return "";
|
||||
}
|
||||
|
||||
function textItemClass(item: MessageStreamItem): string {
|
||||
function textItemClass(item: ThreadStreamItem): string {
|
||||
const classes = ["codex-panel__message", messageRoleClassName(item.role)];
|
||||
if (item.kind === "approvalResult") classes.push("codex-panel__message--approval-result");
|
||||
if (item.kind === "userInputResult") classes.push("codex-panel__message--user-input-result");
|
||||
|
|
@ -176,7 +176,7 @@ function textItemClass(item: MessageStreamItem): string {
|
|||
return classes.join(" ");
|
||||
}
|
||||
|
||||
function messageRoleClassName(role: MessageStreamItem["role"]): string {
|
||||
function messageRoleClassName(role: ThreadStreamItem["role"]): string {
|
||||
if (role === "assistant") return "codex-panel__message--assistant";
|
||||
if (role === "system") return "codex-panel__message--system";
|
||||
if (role === "tool") return "codex-panel__message--tool";
|
||||
303
src/features/chat/presentation/thread-stream/view-model.ts
Normal file
303
src/features/chat/presentation/thread-stream/view-model.ts
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import type { AgentRunSummary, TaskProgressThreadStreamItem, ThreadStreamItem } from "../../domain/thread-stream/items";
|
||||
import { threadStreamItemsEmpty } from "../../domain/thread-stream/selectors";
|
||||
import { activeTurnLiveItems, threadStreamItemsWithoutActiveTaskProgress } from "../../domain/thread-stream/semantics/active-turn";
|
||||
import type { ThreadStreamSemanticClassification } from "../../domain/thread-stream/semantics/types";
|
||||
import type { PendingRequestBlockSnapshot } from "../pending-requests/view-model";
|
||||
import { type DetailView, detailView } from "./detail-view";
|
||||
import { type ThreadStreamItemAnnotations, type ThreadStreamLayoutBlock, threadStreamLayoutBlocks } from "./layout";
|
||||
import { type AgentRunSummaryView, agentRunSummaryView, type ThreadStreamStatusView, threadStreamStatusView } from "./status-view";
|
||||
import { type ThreadStreamTextActionTargets, type ThreadStreamTextView, threadStreamTextView } from "./text-view";
|
||||
|
||||
interface PendingRequestThreadStreamBlockInput {
|
||||
signature: string;
|
||||
snapshot: PendingRequestBlockSnapshot;
|
||||
}
|
||||
|
||||
export interface ThreadStreamPresentationBlockInput {
|
||||
activeThreadId: string | null;
|
||||
activeTurnId: string | null;
|
||||
historyCursor: string | null;
|
||||
loadingHistory: boolean;
|
||||
items: readonly ThreadStreamItem[];
|
||||
stableItems?: readonly ThreadStreamItem[] | undefined;
|
||||
activeItems?: readonly ThreadStreamItem[] | undefined;
|
||||
workspaceRoot?: string | null | undefined;
|
||||
turnDiffs?: ReadonlyMap<string, string> | undefined;
|
||||
textActionTargetsByItemId?: ReadonlyMap<string, ThreadStreamTextActionTargets> | undefined;
|
||||
pendingRequests?: PendingRequestThreadStreamBlockInput | null | undefined;
|
||||
}
|
||||
|
||||
type ThreadStreamPresentationBlock =
|
||||
| {
|
||||
kind: "historyBar";
|
||||
key: "history-bar";
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "empty";
|
||||
key: "empty";
|
||||
}
|
||||
| {
|
||||
kind: "item";
|
||||
key: string;
|
||||
block: Extract<ThreadStreamLayoutBlock, { type: "item" }>;
|
||||
}
|
||||
| {
|
||||
kind: "activityGroup";
|
||||
key: string;
|
||||
block: Extract<ThreadStreamLayoutBlock, { type: "activityGroup" }>;
|
||||
}
|
||||
| {
|
||||
kind: "liveTask";
|
||||
key: string;
|
||||
item: TaskProgressThreadStreamItem;
|
||||
}
|
||||
| {
|
||||
kind: "liveAgentSummary";
|
||||
key: string;
|
||||
summary: AgentRunSummary;
|
||||
}
|
||||
| {
|
||||
kind: "pendingRequests";
|
||||
key: "pending-requests";
|
||||
signature: string;
|
||||
snapshot: PendingRequestBlockSnapshot;
|
||||
};
|
||||
|
||||
type ThreadStreamPresentationBlockSource = (input: ThreadStreamPresentationBlockInput) => readonly ThreadStreamPresentationBlock[];
|
||||
type ThreadStreamRenderFamily = "text" | "detail" | "status";
|
||||
|
||||
export type ThreadStreamRenderedItemView =
|
||||
| {
|
||||
kind: "text";
|
||||
view: ThreadStreamTextView;
|
||||
}
|
||||
| {
|
||||
kind: "detail";
|
||||
view: DetailView;
|
||||
}
|
||||
| {
|
||||
kind: "status";
|
||||
view: ThreadStreamStatusView;
|
||||
};
|
||||
|
||||
export type ThreadStreamActivityItemView =
|
||||
| ({
|
||||
type: "item";
|
||||
id: string;
|
||||
} & ThreadStreamRenderedItemView)
|
||||
| {
|
||||
type: "steering";
|
||||
id: string;
|
||||
label: string;
|
||||
text: string;
|
||||
sourceItemId: string;
|
||||
};
|
||||
|
||||
export type ThreadStreamViewBlock =
|
||||
| {
|
||||
kind: "historyBar";
|
||||
key: "history-bar";
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "empty";
|
||||
key: "empty";
|
||||
}
|
||||
| ({
|
||||
key: string;
|
||||
} & ThreadStreamRenderedItemView)
|
||||
| {
|
||||
kind: "activityGroup";
|
||||
key: string;
|
||||
id: string;
|
||||
turnId: string;
|
||||
summary: string;
|
||||
items: ThreadStreamActivityItemView[];
|
||||
}
|
||||
| {
|
||||
kind: "liveAgentSummary";
|
||||
key: string;
|
||||
view: AgentRunSummaryView;
|
||||
}
|
||||
| {
|
||||
kind: "pendingRequests";
|
||||
key: "pending-requests";
|
||||
signature: string;
|
||||
snapshot: PendingRequestBlockSnapshot;
|
||||
};
|
||||
|
||||
export function threadStreamViewBlocks(input: ThreadStreamPresentationBlockInput): ThreadStreamViewBlock[] {
|
||||
return threadStreamPresentationBlocks(input).map((block) => threadStreamViewBlockFromPresentationBlock(block, input));
|
||||
}
|
||||
|
||||
function threadStreamPresentationBlocks(input: ThreadStreamPresentationBlockInput): ThreadStreamPresentationBlock[] {
|
||||
const headerBlocks = historyPresentationBlocks(input);
|
||||
if (threadStreamItemsEmpty(input)) {
|
||||
return [...headerBlocks, { kind: "empty", key: "empty" }];
|
||||
}
|
||||
|
||||
return [
|
||||
...headerBlocks,
|
||||
...collectPresentationBlocks(input, [layoutPresentationBlocks, activeTurnPresentationBlocks, pendingRequestPresentationBlocks]),
|
||||
];
|
||||
}
|
||||
|
||||
function collectPresentationBlocks(
|
||||
input: ThreadStreamPresentationBlockInput,
|
||||
sources: readonly ThreadStreamPresentationBlockSource[],
|
||||
): ThreadStreamPresentationBlock[] {
|
||||
return sources.flatMap((source) => source(input));
|
||||
}
|
||||
|
||||
function historyPresentationBlocks(input: ThreadStreamPresentationBlockInput): readonly ThreadStreamPresentationBlock[] {
|
||||
if (!input.activeThreadId || !input.historyCursor) return [];
|
||||
return [{ kind: "historyBar", key: "history-bar", loadingHistory: input.loadingHistory }];
|
||||
}
|
||||
|
||||
function layoutPresentationBlocks(input: ThreadStreamPresentationBlockInput): readonly ThreadStreamPresentationBlock[] {
|
||||
return layoutBlocksForInput(input).map(presentationBlockFromLayoutBlock);
|
||||
}
|
||||
|
||||
function presentationBlockFromLayoutBlock(block: ThreadStreamLayoutBlock): ThreadStreamPresentationBlock {
|
||||
if (block.type === "item") return { kind: "item", key: `item:${block.item.id}`, block };
|
||||
return { kind: "activityGroup", key: `activity:${block.id}`, block };
|
||||
}
|
||||
|
||||
function activeTurnPresentationBlocks(input: ThreadStreamPresentationBlockInput): readonly ThreadStreamPresentationBlock[] {
|
||||
if (!input.activeTurnId) return [];
|
||||
return activeTurnLiveBlocks(input, input.activeTurnId);
|
||||
}
|
||||
|
||||
function pendingRequestPresentationBlocks(input: ThreadStreamPresentationBlockInput): readonly ThreadStreamPresentationBlock[] {
|
||||
if (!input.pendingRequests?.signature) return [];
|
||||
return [
|
||||
{
|
||||
kind: "pendingRequests",
|
||||
key: "pending-requests",
|
||||
signature: input.pendingRequests.signature,
|
||||
snapshot: input.pendingRequests.snapshot,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function layoutBlocksForInput(input: ThreadStreamPresentationBlockInput): ThreadStreamLayoutBlock[] {
|
||||
const { activeTurnId } = input;
|
||||
if (!activeTurnId || !input.stableItems || !input.activeItems) {
|
||||
const streamItems = activeTurnId ? threadStreamItemsWithoutActiveTaskProgress(input.items, activeTurnId) : input.items;
|
||||
return threadStreamLayoutBlocks(streamItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
}
|
||||
const stableBlocks = threadStreamLayoutBlocks(input.stableItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
const activeBlocks = threadStreamLayoutBlocks(
|
||||
threadStreamItemsWithoutActiveTaskProgress(input.activeItems, activeTurnId),
|
||||
activeTurnId,
|
||||
input.workspaceRoot,
|
||||
input.turnDiffs,
|
||||
);
|
||||
return [...stableBlocks, ...activeBlocks];
|
||||
}
|
||||
|
||||
function activeTurnLiveBlocks(
|
||||
input: Pick<ThreadStreamPresentationBlockInput, "items" | "activeItems">,
|
||||
activeTurnId: string,
|
||||
): ThreadStreamPresentationBlock[] {
|
||||
return activeTurnLiveItems(input, activeTurnId).map((item): ThreadStreamPresentationBlock => {
|
||||
if (item.kind === "taskProgress") {
|
||||
return {
|
||||
kind: "liveTask",
|
||||
key: `live-task:${item.item.id}`,
|
||||
item: item.item,
|
||||
};
|
||||
}
|
||||
return { kind: "liveAgentSummary", key: `live-agents:${activeTurnId}`, summary: item.summary };
|
||||
});
|
||||
}
|
||||
|
||||
function threadStreamViewBlockFromPresentationBlock(
|
||||
block: ThreadStreamPresentationBlock,
|
||||
input: ThreadStreamPresentationBlockInput,
|
||||
): ThreadStreamViewBlock {
|
||||
if (block.kind === "historyBar" || block.kind === "empty") return block;
|
||||
if (block.kind === "pendingRequests") return block;
|
||||
if (block.kind === "liveAgentSummary") return { kind: "liveAgentSummary", key: block.key, view: agentRunSummaryView(block.summary) };
|
||||
if (block.kind === "liveTask") {
|
||||
return { kind: "status", key: block.key, view: threadStreamStatusView(block.item, statusViewContext(input)) };
|
||||
}
|
||||
if (block.kind === "activityGroup") {
|
||||
return {
|
||||
kind: "activityGroup",
|
||||
key: block.key,
|
||||
id: block.block.id,
|
||||
turnId: block.block.turnId,
|
||||
summary: block.block.summary,
|
||||
items: block.block.items.map((activity) => threadStreamActivityItemView(activity, input)),
|
||||
};
|
||||
}
|
||||
return { key: block.key, ...threadStreamRenderedItemView(block.block.classification, input, block.block.annotations) };
|
||||
}
|
||||
|
||||
function threadStreamActivityItemView(
|
||||
activity: Extract<ThreadStreamLayoutBlock, { type: "activityGroup" }>["items"][number],
|
||||
input: ThreadStreamPresentationBlockInput,
|
||||
): ThreadStreamActivityItemView {
|
||||
if (activity.type === "steering") return activity;
|
||||
return { type: "item", id: activity.id, ...threadStreamRenderedItemView(activity.classification, input) };
|
||||
}
|
||||
|
||||
function threadStreamRenderedItemView(
|
||||
classification: ThreadStreamSemanticClassification,
|
||||
input: ThreadStreamPresentationBlockInput,
|
||||
annotations?: ThreadStreamItemAnnotations,
|
||||
): ThreadStreamRenderedItemView {
|
||||
const renderFamily = threadStreamRenderFamily(classification);
|
||||
switch (renderFamily) {
|
||||
case "text":
|
||||
return {
|
||||
kind: "text",
|
||||
view: threadStreamTextView(classification.item, annotations, {
|
||||
activeTurnId: input.activeTurnId,
|
||||
...definedProp("actionTargets", input.textActionTargetsByItemId?.get(classification.item.id)),
|
||||
}),
|
||||
};
|
||||
case "detail":
|
||||
return { kind: "detail", view: detailView(classification.item, input.workspaceRoot) };
|
||||
case "status":
|
||||
return { kind: "status", view: threadStreamStatusView(classification.item, statusViewContext(input)) };
|
||||
}
|
||||
}
|
||||
|
||||
function threadStreamRenderFamily(classification: ThreadStreamSemanticClassification): ThreadStreamRenderFamily {
|
||||
switch (classification.item.kind) {
|
||||
case "dialogue":
|
||||
case "system":
|
||||
case "userInputResult":
|
||||
return "text";
|
||||
case "command":
|
||||
case "fileChange":
|
||||
case "tool":
|
||||
case "hook":
|
||||
case "goal":
|
||||
case "approvalResult":
|
||||
case "reviewResult":
|
||||
case "agent":
|
||||
return "detail";
|
||||
case "taskProgress":
|
||||
case "reasoning":
|
||||
case "wait":
|
||||
case "contextCompaction":
|
||||
return "status";
|
||||
}
|
||||
return "status";
|
||||
}
|
||||
|
||||
function statusViewContext(input: ThreadStreamPresentationBlockInput): Parameters<typeof threadStreamStatusView>[1] {
|
||||
return {
|
||||
activeTurnId: input.activeTurnId,
|
||||
items: input.items,
|
||||
activeItems: input.activeItems,
|
||||
};
|
||||
}
|
||||
|
||||
function definedProp<Key extends string, Value>(key: Key, value: Value | undefined): Partial<Record<Key, Value>> {
|
||||
return value === undefined ? {} : ({ [key]: value } as Partial<Record<Key, Value>>);
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import type { ApprovalAction, McpElicitationAction, PendingRequestId } from "../../../../domain/pending-requests/model";
|
||||
import type { TurnDiffViewState } from "../../../turn-diff/model";
|
||||
import type { PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import type { MessageStreamForkTarget } from "../../presentation/message-stream/text-view";
|
||||
import type { PlanImplementationTarget } from "../../domain/thread-stream/selectors";
|
||||
import type { PendingRequestBlockSnapshot } from "../../presentation/pending-requests/view-model";
|
||||
import type { ThreadStreamForkTarget } from "../../presentation/thread-stream/text-view";
|
||||
|
||||
export type MessageStreamDisclosureBucket = "details" | "activityGroups" | "textDetails" | "userMessageExpanded" | "approvalDetails";
|
||||
export type ThreadStreamDisclosureBucket = "details" | "activityGroups" | "textDetails" | "userMessageExpanded" | "approvalDetails";
|
||||
|
||||
export interface MessageStreamDisclosureState {
|
||||
export interface ThreadStreamDisclosureState {
|
||||
details: ReadonlySet<string>;
|
||||
activityGroups: ReadonlySet<string>;
|
||||
textDetails: ReadonlySet<string>;
|
||||
|
|
@ -25,8 +25,8 @@ export interface PendingRequestBlockActions {
|
|||
}
|
||||
|
||||
export interface TextItemDetailStateContext {
|
||||
disclosures: MessageStreamDisclosureState;
|
||||
onDisclosureToggle?: (bucket: MessageStreamDisclosureBucket, id: string, open: boolean) => void;
|
||||
disclosures: ThreadStreamDisclosureState;
|
||||
onDisclosureToggle?: (bucket: ThreadStreamDisclosureBucket, id: string, open: boolean) => void;
|
||||
}
|
||||
|
||||
export interface TextItemContentContext extends TextItemDetailStateContext {
|
||||
|
|
@ -40,7 +40,7 @@ export interface TextItemActionContext extends TextItemDetailStateContext {
|
|||
copyText?: (text: string) => void;
|
||||
onImplementPlan?: (target: PlanImplementationTarget) => void;
|
||||
onRollback?: () => void;
|
||||
onFork?: (target: MessageStreamForkTarget, archiveSource: boolean) => void;
|
||||
onFork?: (target: ThreadStreamForkTarget, archiveSource: boolean) => void;
|
||||
}
|
||||
|
||||
export interface TextItemMetadataContext extends TextItemDetailStateContext {
|
||||
|
|
@ -49,7 +49,7 @@ export interface TextItemMetadataContext extends TextItemDetailStateContext {
|
|||
openTurnDiff?: (state: TurnDiffViewState) => void;
|
||||
}
|
||||
|
||||
interface MessageStreamRenderContext {
|
||||
interface ThreadStreamRenderContext {
|
||||
activeThreadId: string | null;
|
||||
workspaceRoot?: string | null;
|
||||
loadOlderTurns: () => void;
|
||||
|
|
@ -59,7 +59,7 @@ interface MessageStreamRenderContext {
|
|||
|
||||
export interface TextItemContext extends TextItemContentContext, TextItemActionContext, TextItemMetadataContext {}
|
||||
|
||||
export interface MessageStreamContext extends MessageStreamRenderContext, TextItemContext {}
|
||||
export interface ThreadStreamContext extends ThreadStreamRenderContext, TextItemContext {}
|
||||
|
||||
export interface PendingRequestBlockContext {
|
||||
signature: string;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import { RawDiffView } from "../../../../shared/ui/diff-view";
|
||||
import type { DetailSection, DetailView } from "../../presentation/message-stream/detail-view";
|
||||
import type { DetailSection, DetailView } from "../../presentation/thread-stream/detail-view";
|
||||
import { OpenAgentThreadAction } from "./agent-thread-action";
|
||||
import type { MessageStreamDisclosureState } from "./context";
|
||||
import type { ThreadStreamDisclosureState } from "./context";
|
||||
|
||||
export interface DetailRenderContext {
|
||||
disclosures: MessageStreamDisclosureState;
|
||||
disclosures: ThreadStreamDisclosureState;
|
||||
onDisclosureToggle?: (bucket: "details", id: string, open: boolean) => void;
|
||||
openThreadInNewView?: (threadId: string) => void;
|
||||
}
|
||||
|
|
@ -8,27 +8,27 @@ type MessageScrollDirection = -1 | 1;
|
|||
const MESSAGE_FLOW_TEXT_LINE_SCROLL_LINES = 4;
|
||||
const MESSAGE_FLOW_REPEATED_TEXT_LINE_SCROLL_LINES = 4;
|
||||
|
||||
export type MessageStreamScrollCommand =
|
||||
export type ThreadStreamScrollCommand =
|
||||
| { kind: "show-latest" }
|
||||
| { kind: "scroll-to"; edge: "start" | "end" }
|
||||
| { kind: "scroll-by"; amount: "text-lines" | "page"; direction: MessageScrollDirection; repeated?: boolean };
|
||||
|
||||
export interface MessageStreamScrollPort {
|
||||
dispatchScrollCommand(command: MessageStreamScrollCommand): void;
|
||||
export interface ThreadStreamScrollPort {
|
||||
dispatchScrollCommand(command: ThreadStreamScrollCommand): void;
|
||||
}
|
||||
|
||||
export interface MessageStreamScrollPortBinding {
|
||||
mountScrollPort(port: MessageStreamScrollPort): () => void;
|
||||
export interface ThreadStreamScrollPortBinding {
|
||||
mountScrollPort(port: ThreadStreamScrollPort): () => void;
|
||||
}
|
||||
|
||||
export interface MessageStreamFlowBlockIdentity {
|
||||
export interface ThreadStreamFlowBlockIdentity {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface MessageStreamFlowFrameProps<Block extends MessageStreamFlowBlockIdentity> {
|
||||
export interface ThreadStreamFlowFrameProps<Block extends ThreadStreamFlowBlockIdentity> {
|
||||
blocks: readonly Block[];
|
||||
rootAttributes?: Partial<Record<`data-${string}`, string>>;
|
||||
scrollPortBinding: MessageStreamScrollPortBinding;
|
||||
scrollPortBinding: ThreadStreamScrollPortBinding;
|
||||
renderBlockContent: (block: Block) => UiNode;
|
||||
}
|
||||
|
||||
|
|
@ -46,9 +46,9 @@ interface MessageFlowRuntime {
|
|||
resizeObserver: ResizeObserver | null;
|
||||
}
|
||||
|
||||
export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity> extends Component<MessageStreamFlowFrameProps<Block>> {
|
||||
export class ThreadStreamFlowFrame<Block extends ThreadStreamFlowBlockIdentity> extends Component<ThreadStreamFlowFrameProps<Block>> {
|
||||
private readonly runtime = createMessageFlowRuntime();
|
||||
private readonly scrollPort: MessageStreamScrollPort = {
|
||||
private readonly scrollPort: ThreadStreamScrollPort = {
|
||||
dispatchScrollCommand: (command) => {
|
||||
applyMessageFlowScrollCommand(this.runtime, command);
|
||||
},
|
||||
|
|
@ -62,12 +62,12 @@ export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity
|
|||
completeMessageFlowRender(this.runtime, null);
|
||||
}
|
||||
|
||||
override getSnapshotBeforeUpdate(previousProps: Readonly<MessageStreamFlowFrameProps<Block>>): MessageFlowSnapshot | null {
|
||||
override getSnapshotBeforeUpdate(previousProps: Readonly<ThreadStreamFlowFrameProps<Block>>): MessageFlowSnapshot | null {
|
||||
return captureMessageFlowSnapshot(this.runtime, this.scrollElement, previousProps.blocks, this.props.blocks);
|
||||
}
|
||||
|
||||
override componentDidUpdate(
|
||||
previousProps: Readonly<MessageStreamFlowFrameProps<Block>>,
|
||||
previousProps: Readonly<ThreadStreamFlowFrameProps<Block>>,
|
||||
_previousState: Readonly<Record<string, never>>,
|
||||
snapshot: MessageFlowSnapshot | null,
|
||||
): void {
|
||||
|
|
@ -82,7 +82,7 @@ export class MessageStreamFlowFrame<Block extends MessageStreamFlowBlockIdentity
|
|||
disposeMessageFlowRuntime(this.runtime);
|
||||
}
|
||||
|
||||
override render({ blocks, renderBlockContent, rootAttributes }: MessageStreamFlowFrameProps<Block>): UiNode {
|
||||
override render({ blocks, renderBlockContent, rootAttributes }: ThreadStreamFlowFrameProps<Block>): UiNode {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
|
|
@ -135,8 +135,8 @@ function createMessageFlowRuntime(): MessageFlowRuntime {
|
|||
function captureMessageFlowSnapshot(
|
||||
runtime: MessageFlowRuntime,
|
||||
container: HTMLElement | null,
|
||||
previousBlocks: readonly MessageStreamFlowBlockIdentity[],
|
||||
nextBlocks: readonly MessageStreamFlowBlockIdentity[],
|
||||
previousBlocks: readonly ThreadStreamFlowBlockIdentity[],
|
||||
nextBlocks: readonly ThreadStreamFlowBlockIdentity[],
|
||||
): MessageFlowSnapshot | null {
|
||||
if (!container || runtime.followingEnd || !messageFlowBlocksShifted(previousBlocks, nextBlocks)) return null;
|
||||
return captureMessageFlowReadingAnchor(container);
|
||||
|
|
@ -206,7 +206,7 @@ function disposeMessageFlowRuntime(runtime: MessageFlowRuntime): void {
|
|||
runtime.followingEnd = false;
|
||||
}
|
||||
|
||||
function applyMessageFlowScrollCommand(runtime: MessageFlowRuntime, command: MessageStreamScrollCommand): void {
|
||||
function applyMessageFlowScrollCommand(runtime: MessageFlowRuntime, command: ThreadStreamScrollCommand): void {
|
||||
switch (command.kind) {
|
||||
case "show-latest":
|
||||
runtime.followingEnd = true;
|
||||
|
|
@ -312,8 +312,8 @@ function cancelMessageFlowEndRestore(runtime: MessageFlowRuntime): void {
|
|||
}
|
||||
|
||||
function messageFlowBlocksShifted(
|
||||
previous: readonly MessageStreamFlowBlockIdentity[],
|
||||
next: readonly MessageStreamFlowBlockIdentity[],
|
||||
previous: readonly ThreadStreamFlowBlockIdentity[],
|
||||
next: readonly ThreadStreamFlowBlockIdentity[],
|
||||
): boolean {
|
||||
if (previous.length === 0 || next.length === 0) return false;
|
||||
if (previous.length !== next.length) return true;
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { ExecutionState } from "../../domain/message-stream/items";
|
||||
import type { AgentRunSummaryView, MessageStreamStatusView } from "../../presentation/message-stream/status-view";
|
||||
import type { MessageStreamContext } from "./context";
|
||||
import type { ExecutionState } from "../../domain/thread-stream/items";
|
||||
import type { AgentRunSummaryView, ThreadStreamStatusView } from "../../presentation/thread-stream/status-view";
|
||||
import type { ThreadStreamContext } from "./context";
|
||||
|
||||
export function agentRunSummaryNode(view: AgentRunSummaryView, context: Pick<MessageStreamContext, "openThreadInNewView">): UiNode {
|
||||
export function agentRunSummaryNode(view: AgentRunSummaryView, context: Pick<ThreadStreamContext, "openThreadInNewView">): UiNode {
|
||||
return <AgentRunSummary view={view} openThreadInNewView={context.openThreadInNewView} />;
|
||||
}
|
||||
|
||||
export function statusNode(view: MessageStreamStatusView): UiNode {
|
||||
export function statusNode(view: ThreadStreamStatusView): UiNode {
|
||||
if (view.kind === "taskProgress") return <TaskProgress view={view} />;
|
||||
if (view.kind === "contextCompaction") return <ContextCompaction view={view} />;
|
||||
if (view.kind === "reasoning") return <Reasoning view={view} />;
|
||||
|
|
@ -42,7 +42,7 @@ function AgentRunSummary({
|
|||
);
|
||||
}
|
||||
|
||||
function TaskProgress({ view }: { view: Extract<MessageStreamStatusView, { kind: "taskProgress" }> }): UiNode {
|
||||
function TaskProgress({ view }: { view: Extract<ThreadStreamStatusView, { kind: "taskProgress" }> }): UiNode {
|
||||
return (
|
||||
<StatusMessage label={view.label} className={view.className} state={view.state}>
|
||||
{view.summary ? <div className="codex-panel__stream-summary">{view.summary}</div> : null}
|
||||
|
|
@ -73,7 +73,7 @@ function TaskProgress({ view }: { view: Extract<MessageStreamStatusView, { kind:
|
|||
);
|
||||
}
|
||||
|
||||
function ContextCompaction({ view }: { view: Extract<MessageStreamStatusView, { kind: "contextCompaction" }> }): UiNode {
|
||||
function ContextCompaction({ view }: { view: Extract<ThreadStreamStatusView, { kind: "contextCompaction" }> }): UiNode {
|
||||
return (
|
||||
<StatusMessage label={view.label} className={view.className} state={view.state}>
|
||||
<div className="codex-panel__stream-summary">{view.text}</div>
|
||||
|
|
@ -81,7 +81,7 @@ function ContextCompaction({ view }: { view: Extract<MessageStreamStatusView, {
|
|||
);
|
||||
}
|
||||
|
||||
function GenericStatus({ view }: { view: Extract<MessageStreamStatusView, { kind: "generic" }> }): UiNode {
|
||||
function GenericStatus({ view }: { view: Extract<ThreadStreamStatusView, { kind: "generic" }> }): UiNode {
|
||||
return (
|
||||
<StatusMessage label={view.label} className={view.className} state={view.state}>
|
||||
<div className="codex-panel__stream-summary">{view.text}</div>
|
||||
|
|
@ -89,7 +89,7 @@ function GenericStatus({ view }: { view: Extract<MessageStreamStatusView, { kind
|
|||
);
|
||||
}
|
||||
|
||||
function Reasoning({ view }: { view: Extract<MessageStreamStatusView, { kind: "reasoning" }> }): UiNode {
|
||||
function Reasoning({ view }: { view: Extract<ThreadStreamStatusView, { kind: "reasoning" }> }): UiNode {
|
||||
return (
|
||||
<div className={`codex-panel__reasoning${view.active ? " is-active" : ""}`}>
|
||||
<div className="codex-panel__reasoning-role">{view.label}</div>
|
||||
|
|
@ -1,51 +1,51 @@
|
|||
import { Fragment, type ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type {
|
||||
MessageStreamActivityItemView,
|
||||
MessageStreamRenderedItemView,
|
||||
MessageStreamViewBlock,
|
||||
} from "../../presentation/message-stream/view-model";
|
||||
import type { MessageStreamContext, PendingRequestBlockContext } from "./context";
|
||||
ThreadStreamActivityItemView,
|
||||
ThreadStreamRenderedItemView,
|
||||
ThreadStreamViewBlock,
|
||||
} from "../../presentation/thread-stream/view-model";
|
||||
import type { PendingRequestBlockContext, ThreadStreamContext } from "./context";
|
||||
import { detailNode } from "./detail";
|
||||
import { MessageStreamFlowFrame, type MessageStreamScrollPortBinding } from "./flow-scroll.measure";
|
||||
import { ThreadStreamFlowFrame, type ThreadStreamScrollPortBinding } from "./flow-scroll.measure";
|
||||
import { pendingRequestBlockNode } from "./pending-request-block";
|
||||
import { agentRunSummaryNode, statusNode } from "./status";
|
||||
import { textNode } from "./text";
|
||||
|
||||
export interface MessageStreamViewportState {
|
||||
blocks: readonly MessageStreamViewBlock[];
|
||||
context: MessageStreamContext;
|
||||
scrollPortBinding: MessageStreamScrollPortBinding;
|
||||
export interface ThreadStreamViewportState {
|
||||
blocks: readonly ThreadStreamViewBlock[];
|
||||
context: ThreadStreamContext;
|
||||
scrollPortBinding: ThreadStreamScrollPortBinding;
|
||||
}
|
||||
|
||||
interface MessageStreamViewportProps {
|
||||
state: MessageStreamViewportState;
|
||||
interface ThreadStreamViewportProps {
|
||||
state: ThreadStreamViewportState;
|
||||
rootAttributes?: Partial<Record<`data-${string}`, string>>;
|
||||
}
|
||||
|
||||
export function MessageStreamViewport({ state, rootAttributes }: MessageStreamViewportProps): UiNode {
|
||||
export function ThreadStreamViewport({ state, rootAttributes }: ThreadStreamViewportProps): UiNode {
|
||||
const { blocks, context, scrollPortBinding } = state;
|
||||
return (
|
||||
<MessageStreamFlowFrame
|
||||
<ThreadStreamFlowFrame
|
||||
blocks={blocks}
|
||||
scrollPortBinding={scrollPortBinding}
|
||||
renderBlockContent={(block) => <MessageStreamBlockContent block={block} context={context} />}
|
||||
renderBlockContent={(block) => <ThreadStreamBlockContent block={block} context={context} />}
|
||||
{...(rootAttributes ? { rootAttributes } : {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function streamItemNode(item: MessageStreamRenderedItemView, context: MessageStreamContext): UiNode {
|
||||
function streamItemNode(item: ThreadStreamRenderedItemView, context: ThreadStreamContext): UiNode {
|
||||
if (item.kind === "text") return textNode(item.view, context);
|
||||
if (item.kind === "detail") return detailNode(item.view, context);
|
||||
return statusNode(item.view);
|
||||
}
|
||||
|
||||
function MessageStreamBlockContent({ block, context }: { block: MessageStreamViewBlock; context: MessageStreamContext }): UiNode {
|
||||
function ThreadStreamBlockContent({ block, context }: { block: ThreadStreamViewBlock; context: ThreadStreamContext }): UiNode {
|
||||
return presentationBlockNode(block, context);
|
||||
}
|
||||
|
||||
function presentationBlockNode(block: MessageStreamViewBlock, context: MessageStreamContext): UiNode {
|
||||
function presentationBlockNode(block: ThreadStreamViewBlock, context: ThreadStreamContext): UiNode {
|
||||
if (block.kind === "historyBar") {
|
||||
return <HistoryBar loadingHistory={block.loadingHistory} loadOlderTurns={context.loadOlderTurns} />;
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ function presentationBlockNode(block: MessageStreamViewBlock, context: MessageSt
|
|||
return streamItemNode(block, context);
|
||||
}
|
||||
|
||||
function pendingRequestContext(context: MessageStreamContext): PendingRequestBlockContext {
|
||||
function pendingRequestContext(context: ThreadStreamContext): PendingRequestBlockContext {
|
||||
if (!context.pendingRequests) throw new Error("Expected pending request context for pending request block.");
|
||||
return context.pendingRequests;
|
||||
}
|
||||
|
|
@ -99,8 +99,8 @@ function ActivityGroup({
|
|||
group,
|
||||
context,
|
||||
}: {
|
||||
group: Extract<MessageStreamViewBlock, { kind: "activityGroup" }>;
|
||||
context: MessageStreamContext;
|
||||
group: Extract<ThreadStreamViewBlock, { kind: "activityGroup" }>;
|
||||
context: ThreadStreamContext;
|
||||
}): UiNode {
|
||||
const open = context.disclosures.activityGroups.has(group.turnId);
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ function ActivityGroup({
|
|||
);
|
||||
}
|
||||
|
||||
function SteeringActivity({ activity }: { activity: Extract<MessageStreamActivityItemView, { type: "steering" }> }): UiNode {
|
||||
function SteeringActivity({ activity }: { activity: Extract<ThreadStreamActivityItemView, { type: "steering" }> }): UiNode {
|
||||
return (
|
||||
<div className="codex-panel__message codex-panel__message--tool codex-panel__detail-item codex-panel__detail codex-panel__detail--plain">
|
||||
<div className="codex-panel__detail-header">
|
||||
|
|
@ -2,13 +2,13 @@ import type { Ref, ComponentChild as UiNode } from "preact";
|
|||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { listenDomEvent, listenOutsideDomEvent } from "../../../../shared/dom/events.dom";
|
||||
import type { MessageStreamTextView } from "../../presentation/message-stream/text-view";
|
||||
import type { ThreadStreamTextView } from "../../presentation/thread-stream/text-view";
|
||||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "./content-rendered-event.dom";
|
||||
import type { TextItemContentContext } from "./context";
|
||||
|
||||
const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360;
|
||||
|
||||
export function CollapsibleTextContent({ view, context }: { view: MessageStreamTextView; context: TextItemContentContext }): UiNode {
|
||||
export function CollapsibleTextContent({ view, context }: { view: ThreadStreamTextView; context: TextItemContentContext }): UiNode {
|
||||
const collapseRef = useRef<HTMLDivElement | null>(null);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [overflows, setOverflows] = useState(false);
|
||||
|
|
@ -68,7 +68,7 @@ export function CollapsibleTextContent({ view, context }: { view: MessageStreamT
|
|||
}
|
||||
|
||||
interface TextContentProps {
|
||||
view: MessageStreamTextView;
|
||||
view: ThreadStreamTextView;
|
||||
context: TextItemContentContext;
|
||||
contentRef?: Ref<HTMLDivElement> | undefined;
|
||||
collapsed?: boolean;
|
||||
|
|
@ -4,19 +4,19 @@ import { IconButton } from "../../../../shared/obsidian/components.obsidian";
|
|||
import type {
|
||||
EditedFilesTextView,
|
||||
MentionedFileTextView,
|
||||
MessageStreamTextView,
|
||||
ReferencedThreadTextView,
|
||||
TextItemDetailSectionView,
|
||||
} from "../../presentation/message-stream/text-view";
|
||||
ThreadStreamTextView,
|
||||
} from "../../presentation/thread-stream/text-view";
|
||||
import type { TextItemActionContext, TextItemContext, TextItemDetailStateContext, TextItemMetadataContext } from "./context";
|
||||
import { closeMessageRoleMenuOnOutsidePointer } from "./text.dom";
|
||||
import { CollapsibleTextContent, TextContent } from "./text-content.dom";
|
||||
|
||||
export function textNode(view: MessageStreamTextView, context: TextItemContext): UiNode {
|
||||
export function textNode(view: ThreadStreamTextView, context: TextItemContext): UiNode {
|
||||
return <Text view={view} context={context} />;
|
||||
}
|
||||
|
||||
function Text({ view, context }: { view: MessageStreamTextView; context: TextItemContext }): UiNode {
|
||||
function Text({ view, context }: { view: ThreadStreamTextView; context: TextItemContext }): UiNode {
|
||||
return (
|
||||
<div className={view.className}>
|
||||
<TextHeader view={view} context={context} />
|
||||
|
|
@ -39,7 +39,7 @@ function Text({ view, context }: { view: MessageStreamTextView; context: TextIte
|
|||
);
|
||||
}
|
||||
|
||||
function TextHeader({ view, context }: { view: MessageStreamTextView; context: TextItemActionContext }): UiNode {
|
||||
function TextHeader({ view, context }: { view: ThreadStreamTextView; context: TextItemActionContext }): UiNode {
|
||||
const forkMenuOpen = context.forkMenuItemId === view.id;
|
||||
const roleRef = useRef<HTMLDivElement | null>(null);
|
||||
const { fork, implementPlan, rollback } = view.actionTargets;
|
||||
|
|
@ -55,7 +55,7 @@ interface ToolbarPrimaryActions {
|
|||
|
||||
interface ToolbarChatActions {
|
||||
startNewThread: () => void;
|
||||
compactConversation: () => void;
|
||||
compactContext: () => void;
|
||||
setGoal: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ function ChatActionsPanel({ model, actions }: { model: ToolbarViewModel; actions
|
|||
className="codex-panel__chat-actions-panel-item"
|
||||
disabled={model.newChatDisabled}
|
||||
/>
|
||||
<ToolbarPanelItem label="Compact context" onClick={actions.compactConversation} className="codex-panel__chat-actions-panel-item" />
|
||||
<ToolbarPanelItem label="Compact context" onClick={actions.compactContext} className="codex-panel__chat-actions-panel-item" />
|
||||
<ToolbarPanelItem label="Set goal..." onClick={actions.setGoal} className="codex-panel__chat-actions-panel-item" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue