diff --git a/src/features/chat/view-model.ts b/src/features/chat/view-model.ts index c3326e00..d875e9b0 100644 --- a/src/features/chat/view-model.ts +++ b/src/features/chat/view-model.ts @@ -15,7 +15,7 @@ import { readRuntimeConfig } from "../../runtime/config"; import { sortedAvailableModels } from "../../runtime/model"; import { compactContextLabel } from "../../runtime/settings"; import { contextSummary, effectiveConfigSections, rateLimitSummary } from "../../runtime/view"; -import { getThreadTitle } from "../../domain/threads/model"; +import { codexPanelDisplayTitle, explicitThreadName, getThreadTitle } from "../../domain/threads/model"; import { connectionDiagnosticSections, diagnosticAlertLevel } from "./diagnostics"; import type { ChatState } from "./chat-state"; import { statusValue, usageLimitStatusLines } from "./status-lines"; @@ -52,6 +52,12 @@ export interface RuntimeToolbarChoicesInput { setRequestedReasoningEffort: (effort: ReasoningEffort | null) => void; } +export interface RestoredThreadTitleSnapshot { + threadId: string; + title: string | null; + explicitName: string | null; +} + export function runtimeSnapshotForChatState({ state }: RuntimeSnapshotInput): RuntimeSnapshot { return { effectiveConfig: state.effectiveConfig, @@ -106,6 +112,30 @@ export function runtimeToolbarChoices(input: RuntimeToolbarChoicesInput): Pick item.id === threadId); + return thread ? getThreadTitle(thread) : null; +} + +export function activeComposerThreadName(state: ChatState, restoredThread: RestoredThreadTitleSnapshot | null): string | null { + const threadId = state.activeThreadId; + if (!threadId) return null; + const thread = state.listedThreads.find((item) => item.id === threadId); + const listedName = thread ? explicitThreadName(thread) : null; + if (listedName) return listedName; + return restoredThread?.threadId === threadId ? restoredThread.explicitName : null; +} + +export function composerPlaceholder(threadName: string | null): string { + return threadName ? `Ask Codex to work on “${threadName}”...` : "Ask Codex to work on this task..."; +} + export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel { const { state, snapshot } = input; const config = readRuntimeConfig(state.effectiveConfig); diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index ca29f3fb..453d3c27 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -30,7 +30,6 @@ import { type ChatAction, type ChatState, } from "./chat-state"; -import { codexPanelDisplayTitle, explicitThreadName, getThreadTitle } from "../../domain/threads/model"; import { referencedThreadDisplay, referencedThreadPrompt, @@ -50,7 +49,11 @@ import { ChatRuntimeSettingsController } from "./runtime-settings-controller"; import { RestoredThreadController } from "./restored-thread-controller"; import { unmountReactRoot } from "../../shared/ui/react-root"; import { + activeComposerThreadName as buildActiveComposerThreadName, + activeThreadTitle as buildActiveThreadTitle, + chatViewDisplayTitle, connectionDiagnosticsModel, + composerPlaceholder as buildComposerPlaceholder, effortStatusLines as buildEffortStatusLines, modelStatusLines as buildModelStatusLines, runtimeToolbarChoices, @@ -366,7 +369,7 @@ export class CodexChatView extends ItemView { } override getDisplayText(): string { - return codexPanelDisplayTitle(this.state.activeThreadId, this.state.listedThreads, this.restoredThreadTitle()); + return chatViewDisplayTitle(this.state, this.restoredThreadTitle()); } override getIcon(): string { @@ -1026,10 +1029,7 @@ export class CodexChatView extends ItemView { } private activeThreadTitle(): string | null { - const threadId = this.state.activeThreadId; - if (!threadId) return null; - const thread = this.state.listedThreads.find((item) => item.id === threadId); - return thread ? getThreadTitle(thread) : null; + return buildActiveThreadTitle(this.state); } private restoredThreadPlaceholder() { @@ -1045,18 +1045,11 @@ export class CodexChatView extends ItemView { } private composerPlaceholder(): string { - const threadName = this.activeComposerThreadName(); - return threadName ? `Ask Codex to work on “${threadName}”...` : "Ask Codex to work on this task..."; + return buildComposerPlaceholder(this.activeComposerThreadName()); } private activeComposerThreadName(): string | null { - const threadId = this.state.activeThreadId; - if (!threadId) return null; - const thread = this.state.listedThreads.find((item) => item.id === threadId); - const listedName = thread ? explicitThreadName(thread) : null; - if (listedName) return listedName; - const restoredThread = this.restoredThreadPlaceholder(); - return restoredThread?.threadId === threadId ? restoredThread.explicitName : null; + return buildActiveComposerThreadName(this.state, this.restoredThreadPlaceholder()); } private readonly renderToolbarSlot = (toolbar: HTMLElement): void => { diff --git a/tests/features/chat/view-model.test.ts b/tests/features/chat/view-model.test.ts index 5f0ded79..8c1eafd8 100644 --- a/tests/features/chat/view-model.test.ts +++ b/tests/features/chat/view-model.test.ts @@ -3,6 +3,10 @@ import { describe, expect, it } from "vitest"; import { createAppServerDiagnostics } from "../../../src/app-server/compatibility"; import { createChatState } from "../../../src/features/chat/chat-state"; import { + activeComposerThreadName, + activeThreadTitle, + chatViewDisplayTitle, + composerPlaceholder, effortStatusLines, runtimeToolbarChoices, modelStatusLines, @@ -94,6 +98,21 @@ describe("chat view model", () => { expect(selectedModels).toEqual(["gpt-5-mini"]); expect(selectedEfforts).toEqual(["high"]); }); + + it("derives active thread titles and composer placeholders", () => { + const state = createChatState(); + state.activeThreadId = "thread-1"; + state.listedThreads = [threadFixture("thread-1", "Active")]; + + expect(chatViewDisplayTitle(state, null)).toBe("Codex: Active"); + expect(activeThreadTitle(state)).toBe("Active"); + expect(activeComposerThreadName(state, null)).toBe("Active"); + expect(composerPlaceholder("Active")).toBe("Ask Codex to work on “Active”..."); + expect(composerPlaceholder(null)).toBe("Ask Codex to work on this task..."); + + state.listedThreads = [threadFixture("thread-1", null)]; + expect(activeComposerThreadName(state, { threadId: "thread-1", title: "Restored", explicitName: "Restored" })).toBe("Restored"); + }); }); function effectiveConfigFixture(config: Record): ConfigReadResponse { @@ -104,7 +123,7 @@ function effectiveConfigFixture(config: Record): ConfigReadResp }; } -function threadFixture(id: string, name: string): Thread { +function threadFixture(id: string, name: string | null): Thread { return { id, sessionId: "session",