diff --git a/src/features/chat/chat-state-selectors.ts b/src/features/chat/chat-state-selectors.ts index 03ffa987..58ab9245 100644 --- a/src/features/chat/chat-state-selectors.ts +++ b/src/features/chat/chat-state-selectors.ts @@ -4,7 +4,7 @@ import type { Thread } from "../../generated/app-server/v2/Thread"; import type { PendingApproval } from "./requests/approvals/model"; import type { PendingUserInput } from "./requests/user-input/model"; import type { DisplayItem } from "./display/types"; -import { implementPlanCandidateFromState } from "./display/plan-implementation"; +import { implementPlanCandidateFromState } from "./display/action-candidates"; export interface PendingRequestSnapshot { approvals: readonly PendingApproval[]; diff --git a/src/features/chat/display/action-candidates.ts b/src/features/chat/display/action-candidates.ts new file mode 100644 index 00000000..8bd20ecb --- /dev/null +++ b/src/features/chat/display/action-candidates.ts @@ -0,0 +1,102 @@ +import { + chatTurnBusy, + type ChatActiveThreadState, + type ChatComposerState, + type ChatRuntimeState, + type ChatTranscriptState, + type ChatTurnState, +} from "../chat-state"; +import type { DisplayItem, MessageDisplayItem } from "./types"; +import { isCompletedTurnOutcomeMessage } from "./turn-outcome-message"; + +export interface ForkCandidate { + itemId: string; + turnId: string; +} + +export interface RollbackCandidate { + turnId: string; + itemId: string; + text: string; +} + +export function forkCandidatesFromItems(items: readonly DisplayItem[]): readonly ForkCandidate[] { + const turnOutcomeItemsByTurn = new Map(); + for (const item of items) { + if (!item.turnId || !isCompletedTurnOutcomeMessage(item)) continue; + turnOutcomeItemsByTurn.set(item.turnId, { itemId: item.id, turnId: item.turnId }); + } + return [...turnOutcomeItemsByTurn.values()]; +} + +export function isForkCandidateItem(item: DisplayItem, candidates: readonly ForkCandidate[]): boolean { + return candidates.some((candidate) => item.id === candidate.itemId && item.turnId === candidate.turnId); +} + +export function turnsAfterTurnId(items: readonly DisplayItem[], turnId: string): number | null { + const turnIds = orderedTurnIds(items); + const index = turnIds.indexOf(turnId); + return index === -1 ? null : turnIds.length - index - 1; +} + +export function rollbackCandidateFromItems(items: readonly DisplayItem[]): RollbackCandidate | null { + const lastTurnId = latestTurnId(items); + if (!lastTurnId) return null; + + const userMessage = items.find((item): item is MessageDisplayItem => isUserMessageForTurn(item, lastTurnId)); + if (!userMessage) return null; + + return { + turnId: lastTurnId, + itemId: userMessage.id, + text: userMessage.text, + }; +} + +export function isRollbackCandidateItem(item: DisplayItem, candidate: RollbackCandidate | null): boolean { + return Boolean( + candidate && item.kind === "message" && item.role === "user" && item.id === candidate.itemId && item.turnId === candidate.turnId, + ); +} + +export function implementPlanCandidateFromState(state: { + activeThread: Pick; + turn: ChatTurnState; + composer: Pick; + runtime: Pick; + transcript: Pick; +}): DisplayItem | null { + if ( + !state.activeThread.id || + chatTurnBusy(state) || + state.composer.draft.trim().length > 0 || + state.runtime.selectedCollaborationMode !== "plan" + ) { + return null; + } + return ( + [...state.transcript.displayItems].reverse().find((item) => item.kind === "message" && item.messageKind === "proposedPlan") ?? null + ); +} + +function orderedTurnIds(items: readonly DisplayItem[]): string[] { + const turnIds: string[] = []; + const seen = new Set(); + for (const item of items) { + if (!item.turnId || seen.has(item.turnId)) continue; + seen.add(item.turnId); + turnIds.push(item.turnId); + } + return turnIds; +} + +function latestTurnId(items: readonly DisplayItem[]): string | null { + for (const item of [...items].reverse()) { + if (item.turnId) return item.turnId; + } + return null; +} + +function isUserMessageForTurn(item: DisplayItem, turnId: string): item is MessageDisplayItem { + return item.kind === "message" && item.role === "user" && item.turnId === turnId; +} diff --git a/src/features/chat/display/fork.ts b/src/features/chat/display/fork.ts deleted file mode 100644 index 658d7cf0..00000000 --- a/src/features/chat/display/fork.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { DisplayItem } from "./types"; -import { isCompletedTurnOutcomeMessage } from "./turn-outcome-message"; - -export interface ForkCandidate { - itemId: string; - turnId: string; -} - -export function forkCandidatesFromItems(items: readonly DisplayItem[]): readonly ForkCandidate[] { - const turnOutcomeItemsByTurn = new Map(); - for (const item of items) { - if (!item.turnId || !isCompletedTurnOutcomeMessage(item)) continue; - turnOutcomeItemsByTurn.set(item.turnId, { itemId: item.id, turnId: item.turnId }); - } - return [...turnOutcomeItemsByTurn.values()]; -} - -export function isForkCandidateItem(item: DisplayItem, candidates: readonly ForkCandidate[]): boolean { - return candidates.some((candidate) => item.id === candidate.itemId && item.turnId === candidate.turnId); -} - -export function turnsAfterTurnId(items: readonly DisplayItem[], turnId: string): number | null { - const turnIds = orderedTurnIds(items); - const index = turnIds.indexOf(turnId); - return index === -1 ? null : turnIds.length - index - 1; -} - -function orderedTurnIds(items: readonly DisplayItem[]): string[] { - const turnIds: string[] = []; - const seen = new Set(); - for (const item of items) { - if (!item.turnId || seen.has(item.turnId)) continue; - seen.add(item.turnId); - turnIds.push(item.turnId); - } - return turnIds; -} diff --git a/src/features/chat/display/plan-implementation.ts b/src/features/chat/display/plan-implementation.ts deleted file mode 100644 index 9a991e66..00000000 --- a/src/features/chat/display/plan-implementation.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - chatTurnBusy, - type ChatActiveThreadState, - type ChatComposerState, - type ChatRuntimeState, - type ChatTranscriptState, - type ChatTurnState, -} from "../chat-state"; -import type { DisplayItem } from "./types"; - -export function implementPlanCandidateFromState(state: { - activeThread: Pick; - turn: ChatTurnState; - composer: Pick; - runtime: Pick; - transcript: Pick; -}): DisplayItem | null { - if ( - !state.activeThread.id || - chatTurnBusy(state) || - state.composer.draft.trim().length > 0 || - state.runtime.selectedCollaborationMode !== "plan" - ) { - return null; - } - return ( - [...state.transcript.displayItems].reverse().find((item) => item.kind === "message" && item.messageKind === "proposedPlan") ?? null - ); -} diff --git a/src/features/chat/display/rollback.ts b/src/features/chat/display/rollback.ts deleted file mode 100644 index e8c3ae04..00000000 --- a/src/features/chat/display/rollback.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { DisplayItem, MessageDisplayItem } from "./types"; - -export interface RollbackCandidate { - turnId: string; - itemId: string; - text: string; -} - -export function rollbackCandidateFromItems(items: readonly DisplayItem[]): RollbackCandidate | null { - const lastTurnId = latestTurnId(items); - if (!lastTurnId) return null; - - const userMessage = items.find((item): item is MessageDisplayItem => isUserMessageForTurn(item, lastTurnId)); - if (!userMessage) return null; - - return { - turnId: lastTurnId, - itemId: userMessage.id, - text: userMessage.text, - }; -} - -export function isRollbackCandidateItem(item: DisplayItem, candidate: RollbackCandidate | null): boolean { - return Boolean( - candidate && item.kind === "message" && item.role === "user" && item.id === candidate.itemId && item.turnId === candidate.turnId, - ); -} - -function latestTurnId(items: readonly DisplayItem[]): string | null { - for (const item of [...items].reverse()) { - if (item.turnId) return item.turnId; - } - return null; -} - -function isUserMessageForTurn(item: DisplayItem, turnId: string): item is MessageDisplayItem { - return item.kind === "message" && item.role === "user" && item.turnId === turnId; -} diff --git a/src/features/chat/panel/model/diagnostics.ts b/src/features/chat/panel/model/diagnostics.ts deleted file mode 100644 index 98878659..00000000 --- a/src/features/chat/panel/model/diagnostics.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { connectionDiagnosticSections } from "../../diagnostics"; -import type { ConnectionDiagnosticsModelInput } from "./types"; - -export function connectionDiagnosticsModel(input: ConnectionDiagnosticsModelInput): ReturnType { - return connectionDiagnosticSections({ - connected: input.connected, - configuredCommand: input.configuredCommand, - initializeResponse: input.state.connection.initializeResponse, - activeThreadCreationCliVersion: input.state.activeThread.creationCliVersion, - diagnostics: input.state.connection.appServerDiagnostics, - }); -} diff --git a/src/features/chat/panel/model/index.ts b/src/features/chat/panel/model/index.ts index 5edbb5da..18dbdb50 100644 --- a/src/features/chat/panel/model/index.ts +++ b/src/features/chat/panel/model/index.ts @@ -1,6 +1,6 @@ export type { ComposerMetaViewModel, RestoredThreadTitleSnapshot, RuntimeChoice } from "./types"; export { composerMetaViewModel, composerPlaceholder } from "./composer"; -export { connectionDiagnosticsModel } from "./diagnostics"; +export { connectionDiagnosticsModel } from "./toolbar"; export { activeComposerThreadName, activeThreadTitle, chatViewDisplayTitle } from "./thread-title"; export { toolbarViewModel } from "./toolbar"; export { effortStatusLines, modelStatusLines, runtimeComposerChoices, runtimeSnapshotForChatState, statusSummaryLines } from "./runtime"; diff --git a/src/features/chat/panel/model/runtime.ts b/src/features/chat/panel/model/runtime.ts index 92044250..048c5928 100644 --- a/src/features/chat/panel/model/runtime.ts +++ b/src/features/chat/panel/model/runtime.ts @@ -7,9 +7,8 @@ import { supportedReasoningEfforts, } from "../../../../runtime/effective-settings"; import { sortedAvailableModels } from "../../../../runtime/models"; -import { contextSummary, rateLimitSummary } from "../../../../runtime/status-summary"; +import { contextSummary, rateLimitSummary, type RateLimitSummary } from "../../../../runtime/status-summary"; import type { ChatState } from "../../chat-state"; -import { statusValue, usageLimitStatusLines } from "./status-lines"; import type { RuntimeChoice, RuntimeComposerChoicesInput, RuntimeSnapshotInput } from "./types"; export function runtimeSnapshotForChatState({ state }: RuntimeSnapshotInput) { @@ -104,3 +103,22 @@ export function effortStatusLines(state: ChatState, snapshot: ReturnType `${row.label}: ${row.value}${row.resetLabel ? ` (${row.resetLabel})` : ""}`)]; +} + +function jsonPreview(value: unknown, fallback: string): string { + try { + return JSON.stringify(value); + } catch { + return fallback; + } +} diff --git a/src/features/chat/panel/model/status-lines.ts b/src/features/chat/panel/model/status-lines.ts deleted file mode 100644 index a595666d..00000000 --- a/src/features/chat/panel/model/status-lines.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RateLimitSummary } from "../../../../runtime/status-summary"; - -export function statusValue(value: unknown, fallback: string): string { - if (typeof value === "string") return value; - if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value); - if (value === null || value === undefined) return fallback; - return jsonPreview(value, fallback); -} - -export function usageLimitStatusLines(limit: RateLimitSummary): string[] { - return ["Usage limits", ...limit.rows.map((row) => `${row.label}: ${row.value}${row.resetLabel ? ` (${row.resetLabel})` : ""}`)]; -} - -function jsonPreview(value: unknown, fallback: string): string { - try { - return JSON.stringify(value); - } catch { - return fallback; - } -} diff --git a/src/features/chat/panel/model/toolbar-model.ts b/src/features/chat/panel/model/toolbar-model.ts deleted file mode 100644 index fb4004d4..00000000 --- a/src/features/chat/panel/model/toolbar-model.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { EffectiveConfigSection, RateLimitSummary } from "../../../../runtime/status-summary"; - -type ToolbarPanelKind = "history" | "chat-actions" | "status"; - -export interface ToolbarThreadRow { - title: string; - threadId: string; - selected: boolean; - disabled: boolean; - canArchive: boolean; - archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean }; - rename: { - draft: string; - generating: boolean; - } | null; -} - -interface ToolbarDiagnosticRow { - label: string; - value: string; - level?: "normal" | "warning" | "error"; -} - -export interface ToolbarDiagnosticSection { - title: string; - rows: ToolbarDiagnosticRow[]; -} - -export interface ToolbarViewModel { - newChatDisabled: boolean; - chatActionsOpen: boolean; - historyOpen: boolean; - statusPanelOpen: boolean; - rateLimit: RateLimitSummary | null; - configSections: EffectiveConfigSection[]; - openPanel: ToolbarPanelKind | null; - threads: ToolbarThreadRow[]; - connectLabel: string; - diagnostics: ToolbarDiagnosticSection[]; -} diff --git a/src/features/chat/panel/model/toolbar.ts b/src/features/chat/panel/model/toolbar.ts index e49d7310..46f1133f 100644 --- a/src/features/chat/panel/model/toolbar.ts +++ b/src/features/chat/panel/model/toolbar.ts @@ -1,9 +1,8 @@ import type { Thread } from "../../../../generated/app-server/v2/Thread"; import { getThreadTitle } from "../../../../domain/threads/model"; import { effectiveConfigSections, rateLimitSummary } from "../../../../runtime/status-summary"; -import type { ToolbarThreadRow, ToolbarViewModel } from "./toolbar-model"; -import { connectionDiagnosticsModel } from "./diagnostics"; -import type { ToolbarViewModelInput } from "./types"; +import { connectionDiagnosticSections } from "../../diagnostics"; +import type { ConnectionDiagnosticsModelInput, ToolbarThreadRow, ToolbarViewModel, ToolbarViewModelInput } from "./types"; export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel { const { state, snapshot } = input; @@ -60,3 +59,13 @@ function toolbarThreadRows(input: { }; }); } + +export function connectionDiagnosticsModel(input: ConnectionDiagnosticsModelInput): ReturnType { + return connectionDiagnosticSections({ + connected: input.connected, + configuredCommand: input.configuredCommand, + initializeResponse: input.state.connection.initializeResponse, + activeThreadCreationCliVersion: input.state.activeThread.creationCliVersion, + diagnostics: input.state.connection.appServerDiagnostics, + }); +} diff --git a/src/features/chat/panel/model/types.ts b/src/features/chat/panel/model/types.ts index aee68404..6192cc02 100644 --- a/src/features/chat/panel/model/types.ts +++ b/src/features/chat/panel/model/types.ts @@ -1,7 +1,7 @@ import type { ReasoningEffort } from "../../../../generated/app-server/ReasoningEffort"; import type { RuntimeSnapshot } from "../../../../runtime/effective-settings"; +import type { EffectiveConfigSection, RateLimitSummary } from "../../../../runtime/status-summary"; import type { ChatState } from "../../chat-state"; -import type { ToolbarThreadRow } from "./toolbar-model"; export interface RuntimeSnapshotInput { state: ChatState; @@ -38,6 +38,43 @@ export interface ComposerContextMeterViewModel { percent: string; } +export interface ToolbarThreadRow { + title: string; + threadId: string; + selected: boolean; + disabled: boolean; + canArchive: boolean; + archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean }; + rename: { + draft: string; + generating: boolean; + } | null; +} + +interface ToolbarDiagnosticRow { + label: string; + value: string; + level?: "normal" | "warning" | "error"; +} + +export interface ToolbarDiagnosticSection { + title: string; + rows: ToolbarDiagnosticRow[]; +} + +export interface ToolbarViewModel { + newChatDisabled: boolean; + chatActionsOpen: boolean; + historyOpen: boolean; + statusPanelOpen: boolean; + rateLimit: RateLimitSummary | null; + configSections: EffectiveConfigSection[]; + openPanel: "history" | "chat-actions" | "status" | null; + threads: ToolbarThreadRow[]; + connectLabel: string; + diagnostics: ToolbarDiagnosticSection[]; +} + export interface ToolbarViewModelInput { state: ChatState; snapshot: RuntimeSnapshot; diff --git a/src/features/chat/panel/slots/types.ts b/src/features/chat/panel/slots/types.ts index 6f449be4..07d4a3dd 100644 --- a/src/features/chat/panel/slots/types.ts +++ b/src/features/chat/panel/slots/types.ts @@ -2,7 +2,7 @@ import type { ReasoningEffort } from "../../../../generated/app-server/Reasoning import type { RuntimeSnapshot } from "../../../../runtime/effective-settings"; import type { SendShortcut } from "../../../../shared/ui/keyboard"; import type { ChatState } from "../../chat-state"; -import type { ToolbarThreadRow } from "../model/toolbar-model"; +import type { ToolbarThreadRow } from "../model/types"; import type { RestoredThreadTitleSnapshot } from "../model"; interface ChatViewToolbarActions { diff --git a/src/features/chat/threads/thread-actions.ts b/src/features/chat/threads/thread-actions.ts index 6ece0255..2b46420c 100644 --- a/src/features/chat/threads/thread-actions.ts +++ b/src/features/chat/threads/thread-actions.ts @@ -6,8 +6,7 @@ import type { ArchiveExportAdapter } from "../../../domain/threads/export"; import { inheritedForkThreadName, upsertThread } from "../../../domain/threads/model"; import type { CodexPanelSettings } from "../../../settings/model"; import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "../chat-state"; -import { turnsAfterTurnId } from "../display/fork"; -import { rollbackCandidateFromItems } from "../display/rollback"; +import { rollbackCandidateFromItems, turnsAfterTurnId } from "../display/action-candidates"; import type { ThreadHistoryController } from "./thread-history-controller"; export interface ChatThreadActionsHost { diff --git a/src/features/chat/ui/message-stream/context-builder.ts b/src/features/chat/ui/message-stream/context-builder.ts index 682ca24b..b6cc2f53 100644 --- a/src/features/chat/ui/message-stream/context-builder.ts +++ b/src/features/chat/ui/message-stream/context-builder.ts @@ -3,9 +3,13 @@ import type { ComponentChild as UiNode } from "preact"; import type { ChatState } from "../../chat-state"; import { chatTurnBusy } from "../../chat-state"; import type { DisplayItem } from "../../display/types"; -import { forkCandidatesFromItems, isForkCandidateItem } from "../../display/fork"; -import { implementPlanCandidateFromState } from "../../display/plan-implementation"; -import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../display/rollback"; +import { + forkCandidatesFromItems, + implementPlanCandidateFromState, + isForkCandidateItem, + isRollbackCandidateItem, + rollbackCandidateFromItems, +} from "../../display/action-candidates"; import type { ChatTurnDiffViewState } from "../turn-diff"; import type { MessageStreamContext } from "./context"; diff --git a/src/features/chat/ui/toolbar.tsx b/src/features/chat/ui/toolbar.tsx index 9c948754..3978f015 100644 --- a/src/features/chat/ui/toolbar.tsx +++ b/src/features/chat/ui/toolbar.tsx @@ -4,7 +4,7 @@ import { useLayoutEffect, useRef } from "preact/hooks"; import type { EffectiveConfigSection, RateLimitSummary } from "../../../runtime/status-summary"; import { IconButton } from "../../../shared/ui/components"; import { renderUiRoot } from "../../../shared/ui/ui-root"; -import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../panel/model/toolbar-model"; +import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../panel/model/types"; type ButtonProps = ButtonHTMLAttributes & { disabled?: boolean | undefined; diff --git a/tests/features/chat/display/fork.test.ts b/tests/features/chat/display/action-candidates.test.ts similarity index 52% rename from tests/features/chat/display/fork.test.ts rename to tests/features/chat/display/action-candidates.test.ts index eb55ec5f..028393d6 100644 --- a/tests/features/chat/display/fork.test.ts +++ b/tests/features/chat/display/action-candidates.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from "vitest"; -import { forkCandidatesFromItems, isForkCandidateItem, turnsAfterTurnId } from "../../../../src/features/chat/display/fork"; +import { + forkCandidatesFromItems, + isForkCandidateItem, + isRollbackCandidateItem, + rollbackCandidateFromItems, + turnsAfterTurnId, +} from "../../../../src/features/chat/display/action-candidates"; import type { DisplayItem } from "../../../../src/features/chat/display/types"; describe("fork candidates", () => { @@ -63,6 +69,58 @@ describe("fork candidates", () => { }); }); +describe("rollback candidate", () => { + it("selects the first user message from the latest turn", () => { + const items: DisplayItem[] = [ + { id: "u1", kind: "message", messageKind: "user", role: "user", text: "older", turnId: "turn-1" }, + { + id: "a1", + kind: "message", + role: "assistant", + text: "older answer", + turnId: "turn-1", + messageKind: "assistantResponse", + messageState: "completed", + }, + { id: "u2", kind: "message", messageKind: "user", role: "user", text: "latest", turnId: "turn-2" }, + { + id: "a2", + kind: "message", + role: "assistant", + text: "latest answer", + turnId: "turn-2", + messageKind: "assistantResponse", + messageState: "completed", + }, + ]; + + const candidate = rollbackCandidateFromItems(items); + + expect(candidate).toEqual({ turnId: "turn-2", itemId: "u2", text: "latest" }); + expect(isRollbackCandidateItem(expectPresent(items[2]), candidate)).toBe(true); + expect(isRollbackCandidateItem(expectPresent(items[0]), candidate)).toBe(false); + expect(isRollbackCandidateItem({ ...expectPresent(items[2]), turnId: "turn-other" }, candidate)).toBe(false); + }); + + it("returns null when there is no completed user turn in display items", () => { + expect(rollbackCandidateFromItems([])).toBeNull(); + expect(rollbackCandidateFromItems([{ id: "system", kind: "system", role: "system", text: "Idle" }])).toBeNull(); + expect( + rollbackCandidateFromItems([ + { + id: "a1", + kind: "message", + role: "assistant", + text: "answer", + turnId: "turn-1", + messageKind: "assistantResponse", + messageState: "completed", + }, + ]), + ).toBeNull(); + }); +}); + function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); return value; diff --git a/tests/features/chat/display/rollback.test.ts b/tests/features/chat/display/rollback.test.ts deleted file mode 100644 index 29f53ba5..00000000 --- a/tests/features/chat/display/rollback.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../../../src/features/chat/display/rollback"; -import type { DisplayItem } from "../../../../src/features/chat/display/types"; - -function expectPresent(value: T | null | undefined): T { - if (value === null || value === undefined) throw new Error("Expected value to be present"); - return value; -} - -describe("rollback candidate", () => { - it("selects the first user message from the latest turn", () => { - const items: DisplayItem[] = [ - { id: "u1", kind: "message", messageKind: "user", role: "user", text: "older", turnId: "turn-1" }, - { - id: "a1", - kind: "message", - role: "assistant", - text: "older answer", - turnId: "turn-1", - messageKind: "assistantResponse", - messageState: "completed", - }, - { id: "u2", kind: "message", messageKind: "user", role: "user", text: "latest", turnId: "turn-2" }, - { - id: "a2", - kind: "message", - role: "assistant", - text: "latest answer", - turnId: "turn-2", - messageKind: "assistantResponse", - messageState: "completed", - }, - ]; - - const candidate = rollbackCandidateFromItems(items); - - expect(candidate).toEqual({ turnId: "turn-2", itemId: "u2", text: "latest" }); - expect(isRollbackCandidateItem(expectPresent(items[2]), candidate)).toBe(true); - expect(isRollbackCandidateItem(expectPresent(items[0]), candidate)).toBe(false); - expect(isRollbackCandidateItem({ ...expectPresent(items[2]), turnId: "turn-other" }, candidate)).toBe(false); - }); - - it("returns null when there is no completed user turn in display items", () => { - expect(rollbackCandidateFromItems([])).toBeNull(); - expect(rollbackCandidateFromItems([{ id: "system", kind: "system", role: "system", text: "Idle" }])).toBeNull(); - expect( - rollbackCandidateFromItems([ - { - id: "a1", - kind: "message", - role: "assistant", - text: "answer", - turnId: "turn-1", - messageKind: "assistantResponse", - messageState: "completed", - }, - ]), - ).toBeNull(); - }); -}); diff --git a/tests/features/chat/panel/status-lines.test.ts b/tests/features/chat/panel/status-lines.test.ts deleted file mode 100644 index 19e24a8b..00000000 --- a/tests/features/chat/panel/status-lines.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { statusValue, usageLimitStatusLines } from "../../../../src/features/chat/panel/model/status-lines"; -import type { RateLimitSummary } from "../../../../src/runtime/status-summary"; - -describe("status line helpers", () => { - it("formats primitive and structured diagnostic values", () => { - expect(statusValue("gpt-5.5", "(default)")).toBe("gpt-5.5"); - expect(statusValue(42, "(default)")).toBe("42"); - expect(statusValue(false, "(default)")).toBe("false"); - expect(statusValue(null, "(default)")).toBe("(default)"); - expect(statusValue({ provider: "openai" }, "(default)")).toBe('{"provider":"openai"}'); - }); - - it("formats usage limit rows for slash command output", () => { - const summary: RateLimitSummary = { - title: "Codex usage limits", - level: "warn", - rows: [ - { - label: "5h", - value: "72%", - percent: 72, - meterDivisions: 5, - level: "warn", - title: "5h usage", - resetLabel: "reset in 2h", - }, - { label: "1w", value: "15%", percent: 15, meterDivisions: 7, level: "ok", title: "1w usage", resetLabel: null }, - ], - }; - - expect(usageLimitStatusLines(summary)).toEqual(["Usage limits", "5h: 72% (reset in 2h)", "1w: 15%"]); - }); -}); diff --git a/tests/features/chat/turns/plan-implementation-actions.test.ts b/tests/features/chat/turns/plan-implementation-actions.test.ts index d4875ea7..64c1e276 100644 --- a/tests/features/chat/turns/plan-implementation-actions.test.ts +++ b/tests/features/chat/turns/plan-implementation-actions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../../../src/app-server/client"; import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../src/features/chat/chat-state"; -import { implementPlanCandidateFromState } from "../../../../src/features/chat/display/plan-implementation"; +import { implementPlanCandidateFromState } from "../../../../src/features/chat/display/action-candidates"; import { createPlanImplementationActions, type PlanImplementationActionsHost, diff --git a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx index 484b0584..c9ad2d03 100644 --- a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx +++ b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from "vitest"; import { act } from "preact/test-utils"; import type { DisplayItem } from "../../../../../src/features/chat/display/types"; -import { implementPlanCandidateFromState } from "../../../../../src/features/chat/display/plan-implementation"; +import { implementPlanCandidateFromState } from "../../../../../src/features/chat/display/action-candidates"; import { topLevelDetailsSummaries } from "../../../../support/dom"; import "./setup"; import { diff --git a/tests/features/chat/ui/renderers/toolbar.test.ts b/tests/features/chat/ui/renderers/toolbar.test.ts index f86856fc..dde3c8b0 100644 --- a/tests/features/chat/ui/renderers/toolbar.test.ts +++ b/tests/features/chat/ui/renderers/toolbar.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; -import type { ToolbarViewModel } from "../../../../../src/features/chat/panel/model/toolbar-model"; +import type { ToolbarViewModel } from "../../../../../src/features/chat/panel/model/types"; import { renderToolbar } from "../../../../../src/features/chat/ui/toolbar"; import { changeInputValue, installObsidianDomShims } from "../../../../support/dom";