Move thread title derivations to view model

This commit is contained in:
murashit 2026-05-29 05:52:57 +09:00
parent 76a1598f04
commit 8632c629e5
3 changed files with 59 additions and 17 deletions

View file

@ -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<T
return { modelChoices, effortChoices };
}
export function chatViewDisplayTitle(state: ChatState, restoredThreadTitle: string | null): string {
return codexPanelDisplayTitle(state.activeThreadId, state.listedThreads, restoredThreadTitle);
}
export function activeThreadTitle(state: ChatState): string | null {
const threadId = state.activeThreadId;
if (!threadId) return null;
const thread = state.listedThreads.find((item) => 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);

View file

@ -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 => {

View file

@ -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<string, unknown>): ConfigReadResponse {
@ -104,7 +123,7 @@ function effectiveConfigFixture(config: Record<string, unknown>): ConfigReadResp
};
}
function threadFixture(id: string, name: string): Thread {
function threadFixture(id: string, name: string | null): Thread {
return {
id,
sessionId: "session",