From 3029bc478d0cb9b08d314e4c152e052be6416473 Mon Sep 17 00:00:00 2001 From: murashit Date: Tue, 9 Jun 2026 10:55:06 +0900 Subject: [PATCH] Constrain generated Thread model to app-server boundary --- src/app-server/shared-cache-state.ts | 12 +++---- src/app-server/shared-cache.ts | 14 ++++---- src/app-server/thread-model.ts | 29 +++++++++++++++++ src/domain/threads/export.ts | 15 +++++---- src/domain/threads/model.ts | 28 +++++++++++----- src/domain/threads/reference.ts | 11 +++---- .../chat/app-server/thread-actions.ts | 32 +++++++++++-------- src/features/chat/chat-host.ts | 8 ++--- src/features/chat/chat-state-actions.ts | 4 +-- src/features/chat/chat-state-selectors.ts | 6 ++-- src/features/chat/chat-state.ts | 14 +++----- src/features/chat/composer/suggestions.ts | 6 ++-- src/features/chat/diagnostics.ts | 2 -- src/features/chat/panel/model/toolbar.ts | 5 ++- src/features/chat/panel/snapshot.ts | 8 ++--- src/features/chat/threads/thread-actions.ts | 12 +++++-- .../chat/threads/thread-rename-controller.ts | 4 +-- .../chat/threads/thread-resume-controller.ts | 7 +++- src/features/chat/threads/thread-resume.ts | 6 ++-- src/features/chat/turns/composition.ts | 2 +- .../chat/turns/slash-command-actions.ts | 4 +-- .../chat/turns/slash-command-execution.ts | 10 +++--- src/features/chat/view.ts | 6 ++-- src/features/thread-picker/modal.ts | 17 +++++----- src/features/threads-view/state.ts | 6 ++-- src/features/threads-view/view.ts | 15 +++++---- src/settings/data.ts | 7 ++-- src/settings/dynamic-sections.ts | 4 +-- src/settings/tab.ts | 8 +++-- src/workspace/thread-surface-coordinator.ts | 4 +-- tests/app-server/shared-cache-state.test.ts | 6 ++-- tests/domain/threads/export.test.ts | 4 ++- tests/domain/threads/reference.test.ts | 5 +-- tests/domain/threads/threads.test.ts | 5 +-- .../chat/chat-app-server-actions.test.ts | 12 ++++--- .../features/chat/chat-state-reducer.test.ts | 3 +- .../composer/composer-suggestions.test.ts | 5 +-- tests/features/chat/diagnostics.test.ts | 2 -- .../chat/display/display-model.test.ts | 2 +- .../features/chat/inbound/controller.test.ts | 5 ++- .../threads/thread-identity-actions.test.ts | 3 +- .../threads/thread-rename-controller.test.ts | 3 +- .../threads/thread-resume-controller.test.ts | 20 ++++++++---- .../chat/threads/thread-resume.test.ts | 5 +-- .../turns/composer-submission-actions.test.ts | 3 +- .../chat/turns/slash-command-actions.test.ts | 3 +- .../turns/slash-command-execution.test.ts | 5 +-- .../turns/turn-submission-controller.test.ts | 3 +- tests/features/chat/view-connection.test.ts | 10 ++++-- tests/features/chat/view-model.test.ts | 3 +- tests/features/thread-picker/modal.test.ts | 5 +-- tests/features/threads-view/renderer.test.ts | 3 +- tests/main.test.ts | 9 +++--- tests/settings/settings-tab.test.ts | 3 +- 54 files changed, 258 insertions(+), 175 deletions(-) create mode 100644 src/app-server/thread-model.ts diff --git a/src/app-server/shared-cache-state.ts b/src/app-server/shared-cache-state.ts index 53842aac..a2ecfc4e 100644 --- a/src/app-server/shared-cache-state.ts +++ b/src/app-server/shared-cache-state.ts @@ -3,7 +3,7 @@ import type { ConfigReadResponse } from "../generated/app-server/v2/ConfigReadRe import type { Model } from "../generated/app-server/v2/Model"; import type { RateLimitSnapshot } from "../generated/app-server/v2/RateLimitSnapshot"; import type { SkillMetadata } from "../generated/app-server/v2/SkillMetadata"; -import type { Thread } from "../generated/app-server/v2/Thread"; +import type { PanelThread } from "../domain/threads/model"; export interface SharedAppServerMetadata { effectiveConfig: ConfigReadResponse | null; @@ -16,7 +16,7 @@ export interface SharedAppServerMetadata { type SharedCache = { kind: "unloaded" } | { kind: "loaded"; data: T }; export interface SharedAppServerState { - threads: SharedCache; + threads: SharedCache; appServerMetadata: SharedCache; availableModels: readonly Model[]; } @@ -29,7 +29,7 @@ export function createSharedAppServerState(): SharedAppServerState { }; } -export function applySharedThreadList(state: SharedAppServerState, threads: readonly Thread[]): SharedAppServerState { +export function applySharedThreadList(state: SharedAppServerState, threads: readonly PanelThread[]): SharedAppServerState { return { ...state, threads: { kind: "loaded", data: cloneThreads(threads) }, @@ -57,7 +57,7 @@ export function applySharedModels(state: SharedAppServerState, models: readonly }; } -export function cachedSharedThreadList(state: SharedAppServerState): readonly Thread[] | null { +export function cachedSharedThreadList(state: SharedAppServerState): readonly PanelThread[] | null { return state.threads.kind === "loaded" ? cloneThreads(state.threads.data) : null; } @@ -82,8 +82,8 @@ function cloneSharedAppServerMetadata(metadata: SharedAppServerMetadata): Shared }; } -function cloneThreads(threads: readonly Thread[]): Thread[] { - return threads.map((thread) => ({ ...thread, turns: [...thread.turns] })); +function cloneThreads(threads: readonly PanelThread[]): PanelThread[] { + return threads.map((thread) => ({ ...thread })); } function cloneModels(models: readonly Model[]): Model[] { diff --git a/src/app-server/shared-cache.ts b/src/app-server/shared-cache.ts index 38a45dfe..6e003a5e 100644 --- a/src/app-server/shared-cache.ts +++ b/src/app-server/shared-cache.ts @@ -1,5 +1,5 @@ -import type { Thread } from "../generated/app-server/v2/Thread"; import type { Model } from "../generated/app-server/v2/Model"; +import type { PanelThread } from "../domain/threads/model"; import { applySharedAppServerMetadata, applySharedModels, @@ -12,16 +12,16 @@ import { type SharedAppServerState, } from "./shared-cache-state"; -type ThreadListRefreshLifecycleState = { kind: "idle" } | { kind: "refreshing"; promise: Promise }; +type ThreadListRefreshLifecycleState = { kind: "idle" } | { kind: "refreshing"; promise: Promise }; export class SharedAppServerCache { private state: SharedAppServerState = createSharedAppServerState(); private threadListRefreshLifecycle: ThreadListRefreshLifecycleState = { kind: "idle" }; refreshThreadList( - fetchThreads: () => Promise, - onSnapshot?: (threads: readonly Thread[]) => void, - ): Promise { + fetchThreads: () => Promise, + onSnapshot?: (threads: readonly PanelThread[]) => void, + ): Promise { if (this.threadListRefreshLifecycle.kind === "refreshing") return this.threadListRefreshLifecycle.promise; const promise = fetchThreads() .then((threads) => { @@ -38,11 +38,11 @@ export class SharedAppServerCache { return promise; } - applyThreadListSnapshot(threads: readonly Thread[]): void { + applyThreadListSnapshot(threads: readonly PanelThread[]): void { this.state = applySharedThreadList(this.state, threads); } - cachedThreadList(): readonly Thread[] | null { + cachedThreadList(): readonly PanelThread[] | null { return cachedSharedThreadList(this.state); } diff --git a/src/app-server/thread-model.ts b/src/app-server/thread-model.ts new file mode 100644 index 00000000..2724fa2c --- /dev/null +++ b/src/app-server/thread-model.ts @@ -0,0 +1,29 @@ +import type { Thread } from "../generated/app-server/v2/Thread"; +import type { PanelThread } from "../domain/threads/model"; + +export function panelThreadFromAppServerThread(thread: Thread, options: { archived?: boolean } = {}): PanelThread { + return { + id: thread.id, + preview: normalizeString(thread.preview), + name: normalizeNullableString(thread.name), + archived: options.archived ?? false, + createdAt: finiteTimestamp(thread.createdAt), + updatedAt: finiteTimestamp(thread.updatedAt), + }; +} + +export function panelThreadsFromAppServerThreads(threads: readonly Thread[], options: { archived?: boolean } = {}): PanelThread[] { + return threads.map((thread) => panelThreadFromAppServerThread(thread, options)); +} + +function normalizeNullableString(value: string | null): string | null { + return value === null ? null : normalizeString(value); +} + +function normalizeString(value: string): string { + return typeof value === "string" ? value : ""; +} + +function finiteTimestamp(value: number): number { + return Number.isFinite(value) ? value : 0; +} diff --git a/src/domain/threads/export.ts b/src/domain/threads/export.ts index 753cf0c4..08dcd589 100644 --- a/src/domain/threads/export.ts +++ b/src/domain/threads/export.ts @@ -1,7 +1,6 @@ -import type { Thread } from "../../generated/app-server/v2/Thread"; import type { Turn } from "../../generated/app-server/v2/Turn"; import { shortThreadId } from "../../utils"; -import { getThreadTitle } from "./model"; +import { getThreadTitle, type PanelThread } from "./model"; import { referencedThreadDisplayFromPrompt } from "./reference"; import { turnTranscriptEntries, type TurnTranscriptEntry } from "./transcript"; @@ -37,8 +36,12 @@ export interface ArchiveExportSettings { vaultPath?: string; } +export interface ArchiveThreadInput extends PanelThread { + turns: Turn[]; +} + export async function exportArchivedThreadMarkdown( - thread: Thread, + thread: ArchiveThreadInput, settings: ArchiveExportSettings, adapter: ArchiveExportAdapter, now = new Date(), @@ -53,7 +56,7 @@ export async function exportArchivedThreadMarkdown( return { path }; } -export function markdownFromThread(thread: Thread, exportedAt = new Date(), settings?: Partial): string { +export function markdownFromThread(thread: ArchiveThreadInput, exportedAt = new Date(), settings?: Partial): string { const title = exportThreadTitle(thread); const tags = normalizedArchiveTags(settings?.archiveExportTags ?? ""); const lines = [ @@ -172,7 +175,7 @@ function stripMatchingQuotes(value: string): string { return (first === `"` || first === `'`) && first === last ? value.slice(1, -1) : value; } -function templateContext(thread: Thread, now: Date): TemplateContext { +function templateContext(thread: ArchiveThreadInput, now: Date): TemplateContext { const title = sanitizePathSegment(exportThreadTitle(thread)); return { date: formatDate(now), @@ -239,7 +242,7 @@ async function uniqueMarkdownPath(adapter: ArchiveExportAdapter, folder: string, return candidate; } -function exportThreadTitle(thread: Thread): string { +function exportThreadTitle(thread: ArchiveThreadInput): string { return getThreadTitle(thread) || "Untitled thread"; } diff --git a/src/domain/threads/model.ts b/src/domain/threads/model.ts index 5a6949c9..dc97ca08 100644 --- a/src/domain/threads/model.ts +++ b/src/domain/threads/model.ts @@ -1,22 +1,34 @@ -import type { Thread } from "../../generated/app-server/v2/Thread"; import { shortThreadId } from "../../utils"; const MAX_THREAD_DISPLAY_TITLE_LENGTH = 96; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -export function getThreadTitle(thread: Thread): string { +export interface PanelThread { + id: string; + preview: string; + name: string | null; + archived: boolean; + createdAt: number; + updatedAt: number; +} + +export function getThreadTitle(thread: PanelThread): string { return ( [thread.name, thread.preview, thread.id].map((value) => (typeof value === "string" ? normalizeTitle(value) : "")).find(Boolean) ?? thread.id ); } -export function explicitThreadName(thread: Thread): string | null { +export function explicitThreadName(thread: PanelThread): string | null { const name = typeof thread.name === "string" ? normalizeTitle(thread.name) : ""; return name.length > 0 ? name : null; } -export function codexPanelDisplayTitle(activeThreadId: string | null, threads: readonly Thread[], fallbackTitle?: string | null): string { +export function codexPanelDisplayTitle( + activeThreadId: string | null, + threads: readonly PanelThread[], + fallbackTitle?: string | null, +): string { if (!activeThreadId) return "Codex"; const thread = threads.find((item) => item.id === activeThreadId); @@ -24,24 +36,24 @@ export function codexPanelDisplayTitle(activeThreadId: string | null, threads: r return title ? `Codex: ${title}` : "Codex"; } -export function inheritedForkThreadName(threadId: string, threads: readonly Thread[]): string | null { +export function inheritedForkThreadName(threadId: string, threads: readonly PanelThread[]): string | null { const thread = threads.find((item) => item.id === threadId); return thread ? explicitThreadName(thread) : null; } -export function upsertThread(threads: readonly Thread[], thread: Thread): Thread[] { +export function upsertThread(threads: readonly PanelThread[], thread: PanelThread): PanelThread[] { const index = threads.findIndex((item) => item.id === thread.id); if (index === -1) return [thread, ...threads]; return threads.map((item, itemIndex) => (itemIndex === index ? { ...item, ...thread } : item)); } -export function archivedThreadDisplayTitle(thread: Thread): string { +export function archivedThreadDisplayTitle(thread: PanelThread): string { const title = fullThreadTitle(thread); if (!title || title === thread.id || UUID_PATTERN.test(title)) return "Untitled archived thread"; return truncateTitle(title, MAX_THREAD_DISPLAY_TITLE_LENGTH); } -function fullThreadTitle(thread: Thread): string { +function fullThreadTitle(thread: PanelThread): string { return normalizeTitle(getThreadTitle(thread)); } diff --git a/src/domain/threads/reference.ts b/src/domain/threads/reference.ts index 041d782c..8e20f0d4 100644 --- a/src/domain/threads/reference.ts +++ b/src/domain/threads/reference.ts @@ -1,8 +1,7 @@ -import type { Thread } from "../../generated/app-server/v2/Thread"; import type { Turn } from "../../generated/app-server/v2/Turn"; import type { UserInput } from "../../generated/app-server/v2/UserInput"; import { shortThreadId } from "../../utils"; -import { getThreadTitle } from "./model"; +import { getThreadTitle, type PanelThread } from "./model"; import { chronologicalTurnConversationSummaries } from "./transcript"; export const REFERENCED_THREAD_TURN_LIMIT = 20; @@ -35,7 +34,7 @@ export function referencedThreadTurns(turns: Turn[]): ReferencedThreadTurn[] { return chronologicalTurnConversationSummaries(turns); } -export function referencedThreadPrompt(thread: Thread, turns: ReferencedThreadTurn[], userRequest: string): string { +export function referencedThreadPrompt(thread: PanelThread, turns: ReferencedThreadTurn[], userRequest: string): string { const reference = referencedThreadDisplay(thread, turns.length); const envelope = referencedThreadEnvelope(reference, userRequest); @@ -58,11 +57,11 @@ export function referencedThreadPrompt(thread: Thread, turns: ReferencedThreadTu ].join("\n"); } -function referencedThreadStatus(thread: Thread, count: number): string { +function referencedThreadStatus(thread: PanelThread, count: number): string { return `Referencing ${shortThreadId(thread.id)} (${String(count)}/${String(REFERENCED_THREAD_TURN_LIMIT)} turns).`; } -function referencedThreadDisplay(thread: Thread, count: number): ReferencedThreadDisplay { +function referencedThreadDisplay(thread: PanelThread, count: number): ReferencedThreadDisplay { return { threadId: thread.id, title: getThreadTitle(thread), @@ -72,7 +71,7 @@ function referencedThreadDisplay(thread: Thread, count: number): ReferencedThrea } export function referencedThreadInput( - thread: Thread, + thread: PanelThread, turns: readonly ReferencedThreadTurn[], userRequest: string, messageInput: UserInput[], diff --git a/src/features/chat/app-server/thread-actions.ts b/src/features/chat/app-server/thread-actions.ts index b4d0a2f7..ffd1759f 100644 --- a/src/features/chat/app-server/thread-actions.ts +++ b/src/features/chat/app-server/thread-actions.ts @@ -1,20 +1,24 @@ -import type { AppServerClient } from "../../../app-server/client"; +import { panelThreadFromAppServerThread, panelThreadsFromAppServerThreads } from "../../../app-server/thread-model"; import { upsertThread } from "../../../domain/threads/model"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import { requestedOrConfiguredServiceTier, type RuntimeSnapshot } from "../../../runtime/effective-settings"; import { resumedThreadAction } from "../threads/thread-resume"; import type { ChatAppServerBaseHost } from "./shared"; +interface StartedThreadSummary { + threadId: string; +} + export interface ChatAppServerThreadActionsHost extends ChatAppServerBaseHost { runtimeSnapshot: () => RuntimeSnapshot; - publishThreadList: (threads: readonly Thread[]) => void; + publishThreadList: (threads: readonly PanelThread[]) => void; syncThreadGoal: (threadId: string) => void; } export interface ChatAppServerThreadActions { - applyThreadList: (threads: readonly Thread[]) => void; - loadThreadList: () => Promise; - startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise> | null>; + applyThreadList: (threads: readonly PanelThread[]) => void; + loadThreadList: () => Promise; + startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise; } export function createChatAppServerThreadActions(host: ChatAppServerThreadActionsHost): ChatAppServerThreadActions { @@ -27,34 +31,34 @@ export function createChatAppServerThreadActions(host: ChatAppServerThreadAction }; } -function applyThreadList(host: ChatAppServerThreadActionsHost, threads: readonly Thread[]): void { +function applyThreadList(host: ChatAppServerThreadActionsHost, threads: readonly PanelThread[]): void { host.stateStore.dispatch({ type: "thread-list/applied", threads, threadsLoaded: true }); } -async function loadThreadList(host: ChatAppServerThreadActionsHost): Promise { +async function loadThreadList(host: ChatAppServerThreadActionsHost): Promise { const client = host.currentClient(); if (!client) throw new Error("Codex app-server is not connected."); const response = await client.listThreads(host.vaultPath); - return response.data; + return panelThreadsFromAppServerThreads(response.data); } async function startThread( host: ChatAppServerThreadActionsHost, preview?: string, options: { syncGoal?: boolean } = {}, -): Promise> | null> { +): Promise { const client = host.currentClient(); if (!client) return null; const serviceTier = requestedOrConfiguredServiceTier(host.runtimeSnapshot()); const response = await client.startThread(host.vaultPath, serviceTier); const state = host.stateStore.getState(); const fallbackPreview = preview?.trim(); - const thread = + const appServerThread = response.thread.preview.trim().length > 0 || !fallbackPreview ? response.thread : { ...response.thread, preview: fallbackPreview }; + const thread = panelThreadFromAppServerThread(appServerThread); const listedThreads = upsertThread(state.threadList.listedThreads, thread); - const resumedResponse = thread === response.thread ? response : { ...response, thread }; - host.stateStore.dispatch(resumedThreadAction({ response: resumedResponse, listedThreads })); + host.stateStore.dispatch(resumedThreadAction({ response: { ...response, thread }, listedThreads })); host.publishThreadList(listedThreads); if (options.syncGoal ?? true) host.syncThreadGoal(response.thread.id); - return response; + return { threadId: response.thread.id }; } diff --git a/src/features/chat/chat-host.ts b/src/features/chat/chat-host.ts index aa4f842e..aa845e8b 100644 --- a/src/features/chat/chat-host.ts +++ b/src/features/chat/chat-host.ts @@ -1,4 +1,4 @@ -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import type { SharedAppServerMetadata } from "../../app-server/shared-cache-state"; import type { CodexPanelSettings } from "../../settings/model"; import type { ChatTurnDiffViewState } from "./ui/turn-diff"; @@ -13,9 +13,9 @@ export interface CodexChatHost { notifyThreadRenamed(threadId: string, name: string | null): void; refreshThreadsViewLiveState(): void; refreshSharedThreadListFromOpenSurface(): void; - applyThreadListSnapshot(threads: readonly Thread[]): void; - refreshThreadList(fetchThreads: () => Promise): Promise; - cachedThreadList(): readonly Thread[] | null; + applyThreadListSnapshot(threads: readonly PanelThread[]): void; + refreshThreadList(fetchThreads: () => Promise): Promise; + cachedThreadList(): readonly PanelThread[] | null; publishAppServerMetadata(metadata: SharedAppServerMetadata): void; cachedAppServerMetadata(): SharedAppServerMetadata | null; } diff --git a/src/features/chat/chat-state-actions.ts b/src/features/chat/chat-state-actions.ts index 203ba23b..55274fae 100644 --- a/src/features/chat/chat-state-actions.ts +++ b/src/features/chat/chat-state-actions.ts @@ -1,5 +1,5 @@ import type { InitializeResponse } from "../../generated/app-server/InitializeResponse"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import type { ThreadTokenUsage } from "../../generated/app-server/v2/ThreadTokenUsage"; import type { ChatAction, PendingTurnStart } from "./chat-state"; import type { DisplayItem } from "./display/types"; @@ -24,7 +24,7 @@ export function clearActiveThreadAction(): ChatAction { return { type: "active-thread/cleared" }; } -export function applyThreadListAction(threads: readonly Thread[], threadsLoaded?: boolean): ChatAction { +export function applyThreadListAction(threads: readonly PanelThread[], threadsLoaded?: boolean): ChatAction { return { type: "thread-list/applied", threads, ...(threadsLoaded === undefined ? {} : { threadsLoaded }) }; } diff --git a/src/features/chat/chat-state-selectors.ts b/src/features/chat/chat-state-selectors.ts index 08b20680..119a285a 100644 --- a/src/features/chat/chat-state-selectors.ts +++ b/src/features/chat/chat-state-selectors.ts @@ -1,6 +1,6 @@ import { activeTurnId as selectActiveTurnId, chatTurnBusy, pendingTurnStart } from "./chat-state"; import type { ChatState, PendingTurnStart } from "./chat-state"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import type { PendingApproval } from "./requests/approval"; import type { PendingUserInput } from "./requests/user-input"; import type { DisplayItem } from "./display/types"; @@ -17,7 +17,7 @@ export interface SubmissionStateSnapshot { activeThreadId: string | null; activeTurnId: string | null; busy: boolean; - listedThreads: readonly Thread[]; + listedThreads: readonly PanelThread[]; displayItems: readonly DisplayItem[]; pendingTurnStart: PendingTurnStart | null; } @@ -39,7 +39,7 @@ export function canSwitchToThread(state: ChatState, threadId: string): boolean { return !chatTurnBusy(state) || threadId === state.activeThread.id; } -export function listedThreads(state: ChatState): readonly Thread[] { +export function listedThreads(state: ChatState): readonly PanelThread[] { return state.threadList.listedThreads; } diff --git a/src/features/chat/chat-state.ts b/src/features/chat/chat-state.ts index 642443dd..f7f826bd 100644 --- a/src/features/chat/chat-state.ts +++ b/src/features/chat/chat-state.ts @@ -8,7 +8,7 @@ import type { ConfigReadResponse } from "../../generated/app-server/v2/ConfigRea import type { Model } from "../../generated/app-server/v2/Model"; import type { RateLimitSnapshot } from "../../generated/app-server/v2/RateLimitSnapshot"; import type { SkillMetadata } from "../../generated/app-server/v2/SkillMetadata"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import type { ThreadGoal } from "../../generated/app-server/v2/ThreadGoal"; import type { ThreadSettingsUpdateParams } from "../../generated/app-server/v2/ThreadSettingsUpdateParams"; import type { ThreadTokenUsage } from "../../generated/app-server/v2/ThreadTokenUsage"; @@ -57,14 +57,13 @@ interface ChatConnectionState { } interface ChatThreadListState { - listedThreads: readonly Thread[]; + listedThreads: readonly PanelThread[]; threadsLoaded: boolean; } export interface ChatActiveThreadState { id: string | null; cwd: string | null; - creationCliVersion: string | null; goal: ThreadGoal | null; tokenUsage: ThreadTokenUsage | null; } @@ -145,7 +144,7 @@ type ConnectionAction = interface ThreadListAppliedAction { type: "thread-list/applied"; - threads?: readonly Thread[]; + threads?: readonly PanelThread[]; threadsLoaded?: boolean; } @@ -153,7 +152,7 @@ type ThreadListAction = ThreadListAppliedAction; export interface ActiveThreadResumedAction { type: "active-thread/resumed"; - thread: Thread; + thread: PanelThread; cwd: string; model: string | null; reasoningEffort: ReasoningEffort | null; @@ -163,7 +162,7 @@ export interface ActiveThreadResumedAction { activePermissionProfile: ActivePermissionProfile | null; displayItems?: readonly DisplayItem[]; status?: string; - listedThreads?: readonly Thread[]; + listedThreads?: readonly PanelThread[]; } export interface ActiveThreadSettingsAppliedAction { @@ -431,7 +430,6 @@ function reduceActiveThreadResumedTransition(state: ChatState, action: ActiveThr activeThread: { id: action.thread.id, cwd: action.cwd, - creationCliVersion: action.thread.cliVersion, goal: null, tokenUsage: null, }, @@ -481,7 +479,6 @@ function reduceActiveThreadRestoredPlaceholderTransition(state: ChatState, actio activeThread: { id: action.threadId, cwd: null, - creationCliVersion: null, goal: null, tokenUsage: null, }, @@ -826,7 +823,6 @@ function initialActiveThreadState(): ChatActiveThreadState { return { id: null, cwd: null, - creationCliVersion: null, goal: null, tokenUsage: null, }; diff --git a/src/features/chat/composer/suggestions.ts b/src/features/chat/composer/suggestions.ts index a6824c63..54b6edac 100644 --- a/src/features/chat/composer/suggestions.ts +++ b/src/features/chat/composer/suggestions.ts @@ -1,6 +1,6 @@ import type { Model } from "../../../generated/app-server/v2/Model"; import type { SkillMetadata } from "../../../generated/app-server/v2/SkillMetadata"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import { prepareFuzzySearch, sortSearchResults, type SearchResult } from "obsidian"; import { findModelByIdOrName, @@ -59,7 +59,7 @@ export function activeComposerSuggestions( beforeCursor: string, notes: NoteCandidate[], skills: readonly SkillMetadata[], - threads: readonly Thread[] = [], + threads: readonly PanelThread[] = [], models: readonly Model[] = [], currentModel: string | null = null, ): ComposerSuggestion[] { @@ -275,7 +275,7 @@ function activeSlashSubcommandSuggestions(beforeCursor: string): ComposerSuggest })); } -function activeThreadCommandSuggestions(beforeCursor: string, threads: readonly Thread[]): ComposerSuggestion[] | null { +function activeThreadCommandSuggestions(beforeCursor: string, threads: readonly PanelThread[]): ComposerSuggestion[] | null { const completion = activeCommandArgumentCompletionQuery(beforeCursor, /^\/(?:resume|refer|archive|rename)\s+([^\s\n]{0,120})$/); if (!completion) return null; diff --git a/src/features/chat/diagnostics.ts b/src/features/chat/diagnostics.ts index 5a1b5951..1c055b09 100644 --- a/src/features/chat/diagnostics.ts +++ b/src/features/chat/diagnostics.ts @@ -18,7 +18,6 @@ export interface ConnectionDiagnosticsInput { connected: boolean; configuredCommand: string; initializeResponse: InitializeResponse | null; - activeThreadCreationCliVersion: string | null; diagnostics: AppServerDiagnostics; } @@ -34,7 +33,6 @@ export function connectionDiagnosticSections(input: ConnectionDiagnosticsInput): { label: "panel client", value: CLIENT_VERSION }, { label: "platform", value: appServerPlatform(input.initializeResponse) }, { label: "Codex home", value: input.initializeResponse?.codexHome ?? "(not connected)" }, - { label: "thread created by CLI", value: input.activeThreadCreationCliVersion ?? "(none)" }, ], }, { diff --git a/src/features/chat/panel/model/toolbar.ts b/src/features/chat/panel/model/toolbar.ts index 46f1133f..9a379fb4 100644 --- a/src/features/chat/panel/model/toolbar.ts +++ b/src/features/chat/panel/model/toolbar.ts @@ -1,4 +1,4 @@ -import type { Thread } from "../../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../../domain/threads/model"; import { getThreadTitle } from "../../../../domain/threads/model"; import { effectiveConfigSections, rateLimitSummary } from "../../../../runtime/status-summary"; import { connectionDiagnosticSections } from "../../diagnostics"; @@ -36,7 +36,7 @@ export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel } function toolbarThreadRows(input: { - threads: readonly Thread[]; + threads: readonly PanelThread[]; activeThreadId: string | null; turnBusy: boolean; archiveConfirmThreadId: string | null; @@ -65,7 +65,6 @@ export function connectionDiagnosticsModel(input: ConnectionDiagnosticsModelInpu 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/snapshot.ts b/src/features/chat/panel/snapshot.ts index 44383ff7..ab58e594 100644 --- a/src/features/chat/panel/snapshot.ts +++ b/src/features/chat/panel/snapshot.ts @@ -1,5 +1,5 @@ import type { Model } from "../../../generated/app-server/v2/Model"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import type { OpenCodexPanelSnapshot } from "../../../workspace/open-panel-snapshot"; import { readRuntimeConfig } from "../../../runtime/config"; import { currentModel } from "../../../runtime/effective-settings"; @@ -145,11 +145,9 @@ function displayItemsSignature(items: readonly DisplayItem[]): string { return stableSignature(items); } -function threadListSignature(threads: readonly Thread[]): string { +function threadListSignature(threads: readonly PanelThread[]): string { return threads - .map((thread) => - signatureParts(thread.id, thread.name, thread.preview, thread.updatedAt, thread.cliVersion, thread.status, thread.gitInfo), - ) + .map((thread) => signatureParts(thread.id, thread.name, thread.preview, thread.updatedAt, thread.archived)) .join("\n"); } diff --git a/src/features/chat/threads/thread-actions.ts b/src/features/chat/threads/thread-actions.ts index ce8d44c7..2d3cfe79 100644 --- a/src/features/chat/threads/thread-actions.ts +++ b/src/features/chat/threads/thread-actions.ts @@ -1,6 +1,7 @@ import { Notice } from "obsidian"; import type { AppServerClient } from "../../../app-server/client"; +import { panelThreadFromAppServerThread } from "../../../app-server/thread-model"; import { exportArchivedThreadMarkdown } from "../../../domain/threads/export"; import type { ArchiveExportAdapter } from "../../../domain/threads/export"; import { inheritedForkThreadName, upsertThread } from "../../../domain/threads/model"; @@ -85,7 +86,11 @@ async function archiveThreadOnServer( const settings = host.settings(); if (saveMarkdown) { const response = await client.readThread(threadId, true); - const result = await exportArchivedThreadMarkdown(response.thread, { ...settings, vaultPath: host.vaultPath }, host.archiveAdapter()); + const result = await exportArchivedThreadMarkdown( + { ...panelThreadFromAppServerThread(response.thread, { archived: true }), turns: response.thread.turns }, + { ...settings, vaultPath: host.vaultPath }, + host.archiveAdapter(), + ); new Notice(`Saved archived thread to ${result.path}.`); } await client.archiveThread(threadId); @@ -176,9 +181,10 @@ async function rollbackThread(host: ChatThreadActionsHost, threadId: string): Pr try { host.setStatus("Rolling back latest turn..."); const response = await client.rollbackThread(threadId); + const thread = panelThreadFromAppServerThread(response.thread); dispatch(host, { type: "active-thread/resumed", - thread: response.thread, + thread, cwd: response.thread.cwd, model: state(host).runtime.activeModel, reasoningEffort: state(host).runtime.activeReasoningEffort, @@ -186,7 +192,7 @@ async function rollbackThread(host: ChatThreadActionsHost, threadId: string): Pr approvalPolicy: state(host).runtime.activeApprovalPolicy, approvalsReviewer: state(host).runtime.activeApprovalsReviewer, activePermissionProfile: state(host).runtime.activePermissionProfile, - listedThreads: upsertThread(state(host).threadList.listedThreads, response.thread), + listedThreads: upsertThread(state(host).threadList.listedThreads, thread), }); dispatch(host, { type: "transcript/items-replaced", diff --git a/src/features/chat/threads/thread-rename-controller.ts b/src/features/chat/threads/thread-rename-controller.ts index b6efa233..c905ec9c 100644 --- a/src/features/chat/threads/thread-rename-controller.ts +++ b/src/features/chat/threads/thread-rename-controller.ts @@ -7,7 +7,7 @@ import { THREAD_NAMING_CONTEXT_UNAVAILABLE_MESSAGE, type ThreadNamingContext, } from "../../../domain/threads/naming"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import type { Turn } from "../../../generated/app-server/v2/Turn"; import type { CodexPanelSettings } from "../../../settings/model"; import type { ChatAction, ChatState, ChatStateStore } from "../chat-state"; @@ -257,7 +257,7 @@ export class ThreadRenameController { return Boolean(this.thread(threadId)?.name?.trim()); } - private thread(threadId: string): Thread | undefined { + private thread(threadId: string): PanelThread | undefined { return this.state.threadList.listedThreads.find((item) => item.id === threadId); } } diff --git a/src/features/chat/threads/thread-resume-controller.ts b/src/features/chat/threads/thread-resume-controller.ts index 2dc9fb05..9cc76b12 100644 --- a/src/features/chat/threads/thread-resume-controller.ts +++ b/src/features/chat/threads/thread-resume-controller.ts @@ -1,4 +1,5 @@ import type { AppServerClient } from "../../../app-server/client"; +import { panelThreadFromAppServerThread } from "../../../app-server/thread-model"; import type { ThreadTokenUsage } from "../../../generated/app-server/v2/ThreadTokenUsage"; import { setActiveThreadTokenUsageAction } from "../chat-state-actions"; import { activeThreadId, canSwitchToThread, displayItemsEmpty, listedThreads } from "../chat-state-selectors"; @@ -46,7 +47,7 @@ export class ThreadResumeController { try { const response = await client.resumeThread(threadId, this.host.vaultPath); if (this.isStale(resume)) return; - this.applyResumedThread(response); + this.applyResumedThread(this.panelThreadActivationResponse(response)); this.recoverResumedThreadTokenUsage(response.thread.id, response.thread.path, resume); if (response.initialTurnsPage) { this.host.history.applyLatestPage(response.thread.id, response.initialTurnsPage); @@ -85,6 +86,10 @@ export class ThreadResumeController { this.host.refreshLiveState(); } + private panelThreadActivationResponse(response: Awaited>): ThreadActivationResponse { + return { ...response, thread: panelThreadFromAppServerThread(response.thread) }; + } + private recoverResumedThreadTokenUsage(threadId: string, path: string | null, resume: ActiveChatResume): void { if (!path || !this.host.recoverTokenUsageFromRollout) return; void this.host diff --git a/src/features/chat/threads/thread-resume.ts b/src/features/chat/threads/thread-resume.ts index 11576cee..a57f22e9 100644 --- a/src/features/chat/threads/thread-resume.ts +++ b/src/features/chat/threads/thread-resume.ts @@ -4,12 +4,12 @@ import type { ReasoningEffort } from "../../../generated/app-server/ReasoningEff import type { ActivePermissionProfile } from "../../../generated/app-server/v2/ActivePermissionProfile"; import type { ApprovalsReviewer } from "../../../generated/app-server/v2/ApprovalsReviewer"; import type { AskForApproval } from "../../../generated/app-server/v2/AskForApproval"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import type { ActiveThreadResumedAction } from "../chat-state"; import type { DisplayItem } from "../display/types"; export interface ThreadActivationResponse { - thread: Thread; + thread: PanelThread; cwd: string; model: string; serviceTier: string | null; @@ -21,7 +21,7 @@ export interface ThreadActivationResponse { export interface ResumedThreadActionParams { response: ThreadActivationResponse; - listedThreads?: readonly Thread[]; + listedThreads?: readonly PanelThread[]; displayItems?: readonly DisplayItem[]; } diff --git a/src/features/chat/turns/composition.ts b/src/features/chat/turns/composition.ts index cda5ca70..8e7f1afa 100644 --- a/src/features/chat/turns/composition.ts +++ b/src/features/chat/turns/composition.ts @@ -101,7 +101,7 @@ export function createConversationSurfaceControllerGroup( startNewThread: thread.startNewThread, startThreadForGoal: async (objective) => { const response = await refs.appServerThreads.startThread(objective, { syncGoal: false }); - return response?.thread.id ?? null; + return response?.threadId ?? null; }, resumeThread: thread.selectThread, forkThread: (threadId) => refs.threadActions.forkThread(threadId), diff --git a/src/features/chat/turns/slash-command-actions.ts b/src/features/chat/turns/slash-command-actions.ts index ac3f6cd2..a4269d34 100644 --- a/src/features/chat/turns/slash-command-actions.ts +++ b/src/features/chat/turns/slash-command-actions.ts @@ -4,7 +4,7 @@ import { referencedThreadTurns, REFERENCED_THREAD_TURN_LIMIT, } from "../../../domain/threads/reference"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult, @@ -128,7 +128,7 @@ async function executeSlashCommand( async function referencedThreadInput( host: SlashCommandActionsHost, client: AppServerClient, - thread: Thread, + thread: PanelThread, message: string, ): Promise { try { diff --git a/src/features/chat/turns/slash-command-execution.ts b/src/features/chat/turns/slash-command-execution.ts index 0d6433a2..e58243cb 100644 --- a/src/features/chat/turns/slash-command-execution.ts +++ b/src/features/chat/turns/slash-command-execution.ts @@ -1,5 +1,5 @@ import type { ReasoningEffort } from "../../../generated/app-server/ReasoningEffort"; -import type { Thread } from "../../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../../domain/threads/model"; import type { ThreadGoal } from "../../../generated/app-server/v2/ThreadGoal"; import type { ThreadGoalStatus } from "../../../generated/app-server/v2/ThreadGoalStatus"; import type { UserInput } from "../../../generated/app-server/v2/UserInput"; @@ -24,11 +24,11 @@ import { export interface SlashCommandExecutionContext { activeThreadId: string | null; busy: boolean; - listedThreads: readonly Thread[]; + listedThreads: readonly PanelThread[]; startNewThread: () => Promise; startThreadForGoal: (objective: string) => Promise; resumeThread: (threadId: string) => Promise; - referThread: (thread: Thread, message: string) => Promise; + referThread: (thread: PanelThread, message: string) => Promise; forkThread: (threadId: string) => Promise; rollbackThread: (threadId: string) => Promise; compactThread: (threadId: string) => Promise; @@ -392,7 +392,7 @@ function lineToRow(line: string): DisplayDetailMetaRow { return { key: "message", value: line }; } -type ThreadResolution = { ok: true; thread: Thread } | { ok: false; message: string }; +type ThreadResolution = { ok: true; thread: PanelThread } | { ok: false; message: string }; function parseReferArgs(args: string): { threadQuery: string; message: string } | null { const parsed = parseThreadAndTextArgs(args); @@ -414,7 +414,7 @@ function parseThreadAndNameArgs(args: string): { threadQuery: string; text: stri return text ? { threadQuery: parsed.threadQuery, text } : null; } -function resolveThreadArgument(args: string, threads: readonly Thread[]): ThreadResolution { +function resolveThreadArgument(args: string, threads: readonly PanelThread[]): ThreadResolution { const query = args.trim(); if (!query) { const thread = threads.at(0); diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index 82f1c20e..fd9f8850 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -5,7 +5,7 @@ import { VIEW_TYPE_CODEX_PANEL } from "../../constants"; import type { DisplayDetailSection, DisplayItem } from "./display/types"; import type { ReasoningEffort } from "../../generated/app-server/ReasoningEffort"; import type { Model } from "../../generated/app-server/v2/Model"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import { collaborationModeLabel as formatCollaborationModeLabel } from "../../runtime/override-commands"; import type { RuntimeSnapshot } from "../../runtime/effective-settings"; import { chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } from "./chat-state"; @@ -347,7 +347,7 @@ export class CodexChatView extends ItemView { try { await this.controllers.connection.controller.ensureConnected(); const response = await this.controllers.appServer.threads.startThread(objective, { syncGoal: false }); - threadId = response?.thread.id ?? null; + threadId = response?.threadId ?? null; } catch (error) { this.controllers.inbound.controller.addSystemMessage(error instanceof Error ? error.message : String(error)); this.controllers.render.controller.render(); @@ -411,7 +411,7 @@ export class CodexChatView extends ItemView { return this.loadSharedThreadList(); } - applyThreadListSnapshot(threads: readonly Thread[]): void { + applyThreadListSnapshot(threads: readonly PanelThread[]): void { this.controllers.appServer.threads.applyThreadList(threads); this.refreshTabHeader(); this.controllers.render.controller.render(); diff --git a/src/features/thread-picker/modal.ts b/src/features/thread-picker/modal.ts index 7e0b8189..333b45ec 100644 --- a/src/features/thread-picker/modal.ts +++ b/src/features/thread-picker/modal.ts @@ -1,8 +1,9 @@ import { Notice, Platform, SuggestModal, type App } from "obsidian"; import { ConnectionManager, StaleConnectionError } from "../../app-server/connection-manager"; +import { panelThreadsFromAppServerThreads } from "../../app-server/thread-model"; import { getThreadTitle } from "../../domain/threads/model"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import type { CodexPanelSettings } from "../../settings/model"; import { shortThreadId } from "../../utils"; @@ -10,14 +11,14 @@ export interface ThreadPickerHost { readonly app: App; readonly settings: CodexPanelSettings; readonly vaultPath: string; - cachedThreadList(): readonly Thread[] | null; - refreshThreadList(fetchThreads: () => Promise): Promise; + cachedThreadList(): readonly PanelThread[] | null; + refreshThreadList(fetchThreads: () => Promise): Promise; openThreadInCurrentView(threadId: string): Promise; openThreadInAvailableView(threadId: string): Promise; } interface ThreadSuggestion { - thread: Thread; + thread: PanelThread; title: string; } @@ -41,7 +42,7 @@ export async function openThreadPicker(host: ThreadPickerHost): Promise { } } -export function threadPickerSuggestions(threads: readonly Thread[], queryText: string): ThreadSuggestion[] { +export function threadPickerSuggestions(threads: readonly PanelThread[], queryText: string): ThreadSuggestion[] { const query = queryText.trim().toLowerCase(); return [...threads] .sort((a, b) => b.updatedAt - a.updatedAt) @@ -78,7 +79,7 @@ export function threadOpenModeFromEvent(evt: MouseEvent | KeyboardEvent): Thread return "current"; } -async function loadThreadPickerThreads(host: ThreadPickerHost): Promise { +async function loadThreadPickerThreads(host: ThreadPickerHost): Promise { const cached = host.cachedThreadList(); if (cached) return cached; @@ -99,7 +100,7 @@ async function loadThreadPickerThreads(host: ThreadPickerHost): Promise { const response = await client.listThreads(host.vaultPath); - return response.data; + return panelThreadsFromAppServerThreads(response.data); }); } finally { connection.disconnect(); @@ -109,7 +110,7 @@ async function loadThreadPickerThreads(host: ThreadPickerHost): Promise { constructor( private readonly host: ThreadPickerHost, - private readonly threads: readonly Thread[], + private readonly threads: readonly PanelThread[], ) { super(host.app); this.limit = MAX_THREAD_PICKER_SUGGESTIONS; diff --git a/src/features/threads-view/state.ts b/src/features/threads-view/state.ts index 86ccea47..c19e1efe 100644 --- a/src/features/threads-view/state.ts +++ b/src/features/threads-view/state.ts @@ -1,5 +1,5 @@ import type { OpenCodexPanelSnapshot } from "../../workspace/open-panel-snapshot"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import { getThreadTitle } from "../../domain/threads/model"; type ThreadsLiveStatus = "needs-input" | "approval" | "running" | "draft" | "offline" | "open"; @@ -12,7 +12,7 @@ export interface ThreadsLiveState { } export interface ThreadsRowModel { - thread: Thread; + thread: PanelThread; title: string; live: ThreadsLiveState | null; selected: boolean; @@ -33,7 +33,7 @@ const STATUS_PRIORITY: Record = { }; export function threadRows( - threads: readonly Thread[], + threads: readonly PanelThread[], snapshots: OpenCodexPanelSnapshot[], renameStates: ReadonlyMap, archiveConfirmThreadId: string | null = null, diff --git a/src/features/threads-view/view.ts b/src/features/threads-view/view.ts index cd5ffee8..20366de1 100644 --- a/src/features/threads-view/view.ts +++ b/src/features/threads-view/view.ts @@ -2,8 +2,9 @@ import { ItemView, Notice, type WorkspaceLeaf } from "obsidian"; import type { AppServerClient } from "../../app-server/client"; import { ConnectionManager, StaleConnectionError } from "../../app-server/connection-manager"; +import { panelThreadFromAppServerThread, panelThreadsFromAppServerThreads } from "../../app-server/thread-model"; import { VIEW_TYPE_CODEX_THREADS } from "../../constants"; -import type { Thread } from "../../generated/app-server/v2/Thread"; +import type { PanelThread } from "../../domain/threads/model"; import type { CodexPanelSettings } from "../../settings/model"; import { exportArchivedThreadMarkdown } from "../../domain/threads/export"; import type { OpenCodexPanelSnapshot } from "../../workspace/open-panel-snapshot"; @@ -38,8 +39,8 @@ export interface CodexThreadsHost { getOpenPanelSnapshots(): OpenCodexPanelSnapshot[]; notifyThreadArchived(threadId: string, options?: { closeOpenPanels?: boolean }): void; notifyThreadRenamed(threadId: string, name: string | null): void; - refreshThreadList(fetchThreads: () => Promise): Promise; - cachedThreadList(): readonly Thread[] | null; + refreshThreadList(fetchThreads: () => Promise): Promise; + cachedThreadList(): readonly PanelThread[] | null; } type ThreadsViewStatus = @@ -56,7 +57,7 @@ export class CodexThreadsView extends ItemView { private connectionLifecycle: ThreadsViewConnectionLifecycleState = { kind: "idle" }; private refreshLifecycle: ThreadsViewRefreshLifecycleState = { kind: "idle" }; private status: ThreadsViewStatus = { kind: "idle" }; - private threads: readonly Thread[] = []; + private threads: readonly PanelThread[] = []; private readonly renameStates = new Map(); private archiveConfirmThreadId: string | null = null; @@ -131,7 +132,7 @@ export class CodexThreadsView extends ItemView { const threads = await this.plugin.refreshThreadList(async () => { if (!this.client) return []; const response = await this.client.listThreads(this.plugin.vaultPath); - return response.data; + return panelThreadsFromAppServerThreads(response.data); }); if (this.isStaleRefresh(refresh)) return; this.threads = threads; @@ -252,7 +253,7 @@ export class CodexThreadsView extends ItemView { this.scheduleRender(); } - applyThreadListSnapshot(threads: readonly Thread[]): void { + applyThreadListSnapshot(threads: readonly PanelThread[]): void { this.threads = threads; this.status = threads.length === 0 ? { kind: "empty", message: "No threads" } : { kind: "idle" }; this.render(); @@ -366,7 +367,7 @@ export class CodexThreadsView extends ItemView { if (saveMarkdown) { const response = await this.client.readThread(threadId, true); const result = await exportArchivedThreadMarkdown( - response.thread, + { ...panelThreadFromAppServerThread(response.thread, { archived: true }), turns: response.thread.turns }, { ...this.plugin.settings, vaultPath: this.plugin.vaultPath }, this.app.vault.adapter, ); diff --git a/src/settings/data.ts b/src/settings/data.ts index b6900936..23e993b3 100644 --- a/src/settings/data.ts +++ b/src/settings/data.ts @@ -1,7 +1,8 @@ import type { AppServerClient } from "../app-server/client"; +import { panelThreadsFromAppServerThreads } from "../app-server/thread-model"; import type { HookMetadata } from "../generated/app-server/v2/HookMetadata"; import type { Model } from "../generated/app-server/v2/Model"; -import type { Thread } from "../generated/app-server/v2/Thread"; +import type { PanelThread } from "../domain/threads/model"; import { errorMessage } from "../utils"; export interface LoadedHooks { @@ -14,7 +15,7 @@ export interface LoadedHooks { export interface SettingsDataLoad { models: SettledSettingsData; hooks: SettledSettingsData; - archivedThreads: SettledSettingsData; + archivedThreads: SettledSettingsData; } type SettledSettingsData = @@ -106,7 +107,7 @@ export async function loadSettingsData(client: AppServerClient, cwd: string): Pr archivedThreadsResult.status === "fulfilled" ? { ok: true, - data: archivedThreadsResult.value.data, + data: panelThreadsFromAppServerThreads(archivedThreadsResult.value.data, { archived: true }), status: `Loaded ${String(archivedThreadsResult.value.data.length)} archived thread${archivedThreadsResult.value.data.length === 1 ? "" : "s"}.`, } : { ok: false, status: `Could not load archived threads: ${errorMessage(archivedThreadsResult.reason)}` }, diff --git a/src/settings/dynamic-sections.ts b/src/settings/dynamic-sections.ts index ee2d756c..d51f1ac7 100644 --- a/src/settings/dynamic-sections.ts +++ b/src/settings/dynamic-sections.ts @@ -1,7 +1,7 @@ import { Setting } from "obsidian"; import type { HookMetadata } from "../generated/app-server/v2/HookMetadata"; -import type { Thread } from "../generated/app-server/v2/Thread"; +import type { PanelThread } from "../domain/threads/model"; import { archivedThreadDisplayTitle } from "../domain/threads/model"; import { shortThreadId } from "../utils"; @@ -10,7 +10,7 @@ export interface ArchivedThreadSectionState { exportFolderTemplate: string; exportFilenameTemplate: string; exportTags: string; - threads: Thread[]; + threads: PanelThread[]; loaded: boolean; loading: boolean; status: string; diff --git a/src/settings/tab.ts b/src/settings/tab.ts index 9b90e58b..331effbb 100644 --- a/src/settings/tab.ts +++ b/src/settings/tab.ts @@ -2,11 +2,12 @@ import { type App, Notice, type Plugin, PluginSettingTab, Setting, setIcon } fro import type { AppServerClient } from "../app-server/client"; import { withShortLivedAppServerClient } from "../app-server/short-lived-client"; +import { panelThreadFromAppServerThread } from "../app-server/thread-model"; import { DEFAULT_CODEX_PATH } from "../constants"; import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort"; import type { HookMetadata } from "../generated/app-server/v2/HookMetadata"; import type { Model } from "../generated/app-server/v2/Model"; -import type { Thread } from "../generated/app-server/v2/Thread"; +import type { PanelThread } from "../domain/threads/model"; import { findModelByIdOrName, REASONING_EFFORTS, sortedAvailableModels, supportedEffortsForModel } from "../runtime/models"; import { archivedThreadDisplayTitle } from "../domain/threads/model"; import { errorMessage } from "../utils"; @@ -37,7 +38,7 @@ export class CodexPanelSettingTab extends PluginSettingTab { private settingsDataAutoLoadStarted = false; private settingsDynamicOperationId = 0; private settingsDataRefreshLifecycle: SettingsDataRefreshLifecycleState = { kind: "idle" }; - private archivedThreads: Thread[] = []; + private archivedThreads: PanelThread[] = []; private archivedThreadsLifecycle = createSettingsDynamicSectionLifecycle(); private hooks: HookMetadata[] = []; private hookWarnings: string[] = []; @@ -480,9 +481,10 @@ export class CodexPanelSettingTab extends PluginSettingTab { try { const response = await this.withSettingsConnection((client) => client.unarchiveThread(threadId)); this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId); + const restoredThread = panelThreadFromAppServerThread(response.thread); this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { type: "loaded", - status: `Restored "${archivedThreadDisplayTitle(response.thread)}".`, + status: `Restored "${archivedThreadDisplayTitle(restoredThread)}".`, operationId, }); this.plugin.refreshSharedThreadListFromOpenSurface(); diff --git a/src/workspace/thread-surface-coordinator.ts b/src/workspace/thread-surface-coordinator.ts index 09990c59..486cda78 100644 --- a/src/workspace/thread-surface-coordinator.ts +++ b/src/workspace/thread-surface-coordinator.ts @@ -3,7 +3,7 @@ import type { App } from "obsidian"; import { VIEW_TYPE_CODEX_THREADS } from "../constants"; import { CodexThreadsView } from "../features/threads-view/view"; import type { Model } from "../generated/app-server/v2/Model"; -import type { Thread } from "../generated/app-server/v2/Thread"; +import type { PanelThread } from "../domain/threads/model"; import type { SharedAppServerMetadata } from "../app-server/shared-cache-state"; import type { WorkspacePanelCoordinator } from "./panel-coordinator"; @@ -33,7 +33,7 @@ export class ThreadSurfaceCoordinator { if (threadsView) void threadsView.refresh(); } - applyThreadListSnapshot(threads: readonly Thread[]): void { + applyThreadListSnapshot(threads: readonly PanelThread[]): void { for (const view of this.options.panels.panelViews()) { view.applyThreadListSnapshot(threads); } diff --git a/tests/app-server/shared-cache-state.test.ts b/tests/app-server/shared-cache-state.test.ts index 79abafde..5c9912c8 100644 --- a/tests/app-server/shared-cache-state.test.ts +++ b/tests/app-server/shared-cache-state.test.ts @@ -10,6 +10,7 @@ import { cachedSharedThreadList, createSharedAppServerState, } from "../../src/app-server/shared-cache-state"; +import type { PanelThread } from "../../src/domain/threads/model"; import type { Model } from "../../src/generated/app-server/v2/Model"; import type { Thread } from "../../src/generated/app-server/v2/Thread"; @@ -22,7 +23,7 @@ describe("shared app-server cache state", () => { const cachedThreads = cachedSharedThreadList(threadState); expect(cachedThreads?.map((thread) => thread.id)).toEqual(["thread-1"]); - const mutableCachedThreads = cachedThreads as Thread[]; + const mutableCachedThreads = cachedThreads as PanelThread[]; mutableCachedThreads.push(threadFixture("thread-3")); expect(cachedSharedThreadList(threadState)?.map((thread) => thread.id)).toEqual(["thread-1"]); @@ -50,7 +51,7 @@ describe("shared app-server cache state", () => { }); }); -function threadFixture(id: string): Thread { +function threadFixture(id: string): Thread & { archived: boolean } { return { id, sessionId: "session", @@ -71,6 +72,7 @@ function threadFixture(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } diff --git a/tests/domain/threads/export.test.ts b/tests/domain/threads/export.test.ts index 45e7ba2c..08c89989 100644 --- a/tests/domain/threads/export.test.ts +++ b/tests/domain/threads/export.test.ts @@ -18,6 +18,7 @@ describe("thread archive export", () => { thread({ id: "thread-12345678", name: "Exported thread", + archived: false, turns: [ turn( [ @@ -267,7 +268,7 @@ class MemoryAdapter implements ArchiveExportAdapter { } } -function thread(overrides: Partial = {}): Thread { +function thread(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", sessionId: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", @@ -288,6 +289,7 @@ function thread(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], ...overrides, }; diff --git a/tests/domain/threads/reference.test.ts b/tests/domain/threads/reference.test.ts index 4990c05d..ad583e4f 100644 --- a/tests/domain/threads/reference.test.ts +++ b/tests/domain/threads/reference.test.ts @@ -9,7 +9,7 @@ import { referencedThreadTurns, } from "../../../src/domain/threads/reference"; -function thread(overrides: Partial = {}): Thread { +function thread(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "019abcde-0000-7000-8000-000000000001", sessionId: "session-1", @@ -30,9 +30,10 @@ function thread(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: "参照元", + archived: false, turns: [], ...overrides, - } as Thread; + } as Thread & { archived: boolean }; } function turn(id: string, startedAt: number, items: Turn["items"]): Turn { diff --git a/tests/domain/threads/threads.test.ts b/tests/domain/threads/threads.test.ts index 1b6d74b9..c5e5db34 100644 --- a/tests/domain/threads/threads.test.ts +++ b/tests/domain/threads/threads.test.ts @@ -48,7 +48,7 @@ describe("thread helpers", () => { }); }); -function thread(overrides: Partial = {}): Thread { +function thread(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "thread-1", sessionId: "session-1", @@ -69,7 +69,8 @@ function thread(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], ...overrides, - } as Thread; + } as Thread & { archived: boolean }; } diff --git a/tests/features/chat/chat-app-server-actions.test.ts b/tests/features/chat/chat-app-server-actions.test.ts index 44e4ddd8..13652c45 100644 --- a/tests/features/chat/chat-app-server-actions.test.ts +++ b/tests/features/chat/chat-app-server-actions.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../../src/app-server/client"; +import { panelThreadFromAppServerThread } from "../../../src/app-server/thread-model"; import { createChatAppServerDiagnosticsActions } from "../../../src/features/chat/app-server/diagnostics-actions"; import { createChatAppServerMetadataActions } from "../../../src/features/chat/app-server/metadata-actions"; import { createChatAppServerThreadActions } from "../../../src/features/chat/app-server/thread-actions"; @@ -15,10 +16,11 @@ describe("chat app-server controllers", () => { it("publishes newly started threads before the first turn completes", async () => { const state = createChatState(); const existing = threadFixture("existing"); - state.threadList.listedThreads = [existing]; + state.threadList.listedThreads = [panelThreadFromAppServerThread(existing)]; const stateStore = createChatStateStore(state); const started = threadFixture("started"); - const optimistic = { ...started, preview: "first prompt" }; + const optimistic = panelThreadFromAppServerThread({ ...started, preview: "first prompt" }); + const existingPanelThread = panelThreadFromAppServerThread(existing); const publishThreadList = vi.fn(); const syncThreadGoal = vi.fn(); const client = { @@ -45,8 +47,8 @@ describe("chat app-server controllers", () => { await controller.startThread("first prompt"); - expect(stateStore.getState().threadList.listedThreads).toEqual([optimistic, existing]); - expect(publishThreadList).toHaveBeenCalledWith([optimistic, existing]); + expect(stateStore.getState().threadList.listedThreads).toEqual([optimistic, existingPanelThread]); + expect(publishThreadList).toHaveBeenCalledWith([optimistic, existingPanelThread]); expect(syncThreadGoal).toHaveBeenCalledWith("started"); }); @@ -109,7 +111,7 @@ describe("chat app-server controllers", () => { await controller.startThread("local preview"); - expect(publishThreadList).toHaveBeenCalledWith([started]); + expect(publishThreadList).toHaveBeenCalledWith([panelThreadFromAppServerThread(started)]); }); it("reuses cached app-server metadata for deferred diagnostics", async () => { diff --git a/tests/features/chat/chat-state-reducer.test.ts b/tests/features/chat/chat-state-reducer.test.ts index 9702f8c2..23c4dbe3 100644 --- a/tests/features/chat/chat-state-reducer.test.ts +++ b/tests/features/chat/chat-state-reducer.test.ts @@ -554,7 +554,7 @@ function goal(threadId: string): ThreadGoal { }; } -export function thread(id: string): Thread { +export function thread(id: string): Thread & { archived: boolean } { return { id, sessionId: "session", @@ -575,6 +575,7 @@ export function thread(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } diff --git a/tests/features/chat/composer/composer-suggestions.test.ts b/tests/features/chat/composer/composer-suggestions.test.ts index d81a83ac..8a269173 100644 --- a/tests/features/chat/composer/composer-suggestions.test.ts +++ b/tests/features/chat/composer/composer-suggestions.test.ts @@ -19,7 +19,7 @@ function expectPresent(value: T | null | undefined): T { return value; } -function thread(overrides: Partial = {}): Thread { +function thread(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "019abcde-0000-7000-8000-000000000001", sessionId: "session-1", @@ -40,9 +40,10 @@ function thread(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], ...overrides, - } as Thread; + } as Thread & { archived: boolean }; } function model(name: string, efforts: ReasoningEffort[], overrides: Partial = {}): Model { diff --git a/tests/features/chat/diagnostics.test.ts b/tests/features/chat/diagnostics.test.ts index 0777ba2b..2858b289 100644 --- a/tests/features/chat/diagnostics.test.ts +++ b/tests/features/chat/diagnostics.test.ts @@ -37,7 +37,6 @@ describe("connection diagnostics", () => { platformFamily: "unix", platformOs: "macos", }, - activeThreadCreationCliVersion: "0.130.0", diagnostics, }); @@ -46,7 +45,6 @@ describe("connection diagnostics", () => { expect(rows.map((row) => `${row.label}: ${row.value}`)).toEqual( expect.arrayContaining([ "connection: connected", - "thread created by CLI: 0.130.0", "model/list: ok (12 models)", "skills/list: failed - unknown method skills/list", "mcp docs: ready - auth notLoggedIn", diff --git a/tests/features/chat/display/display-model.test.ts b/tests/features/chat/display/display-model.test.ts index a0f4805a..6848e511 100644 --- a/tests/features/chat/display/display-model.test.ts +++ b/tests/features/chat/display/display-model.test.ts @@ -108,7 +108,7 @@ describe("thread item conversion preserves app-server semantics", () => { it("hides persisted /refer context in displayed user messages", () => { const text = referencedThreadPrompt( - { id: "thread-reference", name: "参照元", preview: "", turns: [] } as unknown as Thread, + { id: "thread-reference", name: "参照元", preview: "", archived: false } as Thread & { archived: boolean }, [ { userText: "元の依頼", assistantText: "元の回答" }, { userText: "次の依頼", assistantText: "次の回答" }, diff --git a/tests/features/chat/inbound/controller.test.ts b/tests/features/chat/inbound/controller.test.ts index c39015d6..d07eb359 100644 --- a/tests/features/chat/inbound/controller.test.ts +++ b/tests/features/chat/inbound/controller.test.ts @@ -953,7 +953,6 @@ describe("ChatInboundController", () => { state.turn.lifecycle = { kind: "running", turnId: "turn-active" }; state.runtime.activeModel = "gpt-5.5"; state.runtime.activeServiceTier = "fast"; - state.activeThread.creationCliVersion = "codex-cli 1.0.0"; state.activeThread.tokenUsage = { last: { inputTokens: 1, cachedInputTokens: 0, outputTokens: 2, reasoningOutputTokens: 0, totalTokens: 3 }, total: { inputTokens: 1, cachedInputTokens: 0, outputTokens: 2, reasoningOutputTokens: 0, totalTokens: 3 }, @@ -1001,7 +1000,6 @@ describe("ChatInboundController", () => { expect(activeTurnId(state)).toBeNull(); expect(state.runtime.activeModel).toBeNull(); expect(state.runtime.activeServiceTier).toBeNull(); - expect(state.activeThread.creationCliVersion).toBeNull(); expect(state.activeThread.tokenUsage).toBeNull(); expect(state.transcript.historyCursor).toBeNull(); expect(state.transcript.loadingHistory).toBe(false); @@ -1712,7 +1710,7 @@ function userInputRequest(id: number): ServerRequest { }; } -function thread(id: string, cwd: string): Thread { +function thread(id: string, cwd: string): Thread & { archived: boolean } { return { id, sessionId: id, @@ -1733,6 +1731,7 @@ function thread(id: string, cwd: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } diff --git a/tests/features/chat/threads/thread-identity-actions.test.ts b/tests/features/chat/threads/thread-identity-actions.test.ts index 2f0e5c4f..6ce29882 100644 --- a/tests/features/chat/threads/thread-identity-actions.test.ts +++ b/tests/features/chat/threads/thread-identity-actions.test.ts @@ -6,7 +6,7 @@ import type { RestoredThreadController } from "../../../../src/features/chat/thr import type { RestoredThreadPlaceholderState } from "../../../../src/features/chat/panel/lifecycle"; import type { Thread } from "../../../../src/generated/app-server/v2/Thread"; -function thread(id: string, name: string | null = null): Thread { +function thread(id: string, name: string | null = null): Thread & { archived: boolean } { return { id, sessionId: id, @@ -27,6 +27,7 @@ function thread(id: string, name: string | null = null): Thread { agentRole: null, gitInfo: null, name, + archived: false, turns: [], }; } diff --git a/tests/features/chat/threads/thread-rename-controller.test.ts b/tests/features/chat/threads/thread-rename-controller.test.ts index 80935170..099fea00 100644 --- a/tests/features/chat/threads/thread-rename-controller.test.ts +++ b/tests/features/chat/threads/thread-rename-controller.test.ts @@ -147,7 +147,7 @@ function fakeClient(options: { setThreadName?: ReturnType } = {}): } as unknown as AppServerClient; } -function threadFixture(id: string): Thread { +function threadFixture(id: string): Thread & { archived: boolean } { return { id, sessionId: "session", @@ -168,6 +168,7 @@ function threadFixture(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } diff --git a/tests/features/chat/threads/thread-resume-controller.test.ts b/tests/features/chat/threads/thread-resume-controller.test.ts index e9f4377c..d6e3219f 100644 --- a/tests/features/chat/threads/thread-resume-controller.test.ts +++ b/tests/features/chat/threads/thread-resume-controller.test.ts @@ -3,16 +3,16 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../../../src/app-server/client"; import { createChatState, createChatStateStore } from "../../../../src/features/chat/chat-state"; import type { RestoredThreadController } from "../../../../src/features/chat/threads/restored-thread-controller"; -import type { ThreadActivationResponse } from "../../../../src/features/chat/threads/thread-resume"; import { ThreadResumeController } from "../../../../src/features/chat/threads/thread-resume-controller"; import type { ThreadHistoryController } from "../../../../src/features/chat/threads/thread-history-controller"; import { ChatResumeWorkTracker } from "../../../../src/features/chat/panel/lifecycle"; +import type { ThreadResumeResponse } from "../../../../src/generated/app-server/v2/ThreadResumeResponse"; import type { ThreadItem } from "../../../../src/generated/app-server/v2/ThreadItem"; import type { Thread } from "../../../../src/generated/app-server/v2/Thread"; import type { ThreadTokenUsage } from "../../../../src/generated/app-server/v2/ThreadTokenUsage"; import type { Turn } from "../../../../src/generated/app-server/v2/Turn"; -function thread(id: string): Thread { +function thread(id: string): Thread & { archived: boolean } { return { id, sessionId: id, @@ -33,25 +33,31 @@ function thread(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } -function activation(threadId: string): ThreadActivationResponse { +function activation(threadId: string): ThreadResumeResponse { return { thread: thread(threadId), cwd: "/vault", model: "gpt-test", + modelProvider: "openai", serviceTier: null, - approvalPolicy: null, - approvalsReviewer: null, + runtimeWorkspaceRoots: [], + instructionSources: [], + approvalPolicy: "on-request", + approvalsReviewer: "user", + sandbox: { type: "readOnly", networkAccess: false }, activePermissionProfile: null, reasoningEffort: null, + initialTurnsPage: null, }; } function createController( - response: ThreadActivationResponse = activation("thread"), + response: ThreadResumeResponse = activation("thread"), overrides: Partial[0]> = {}, ) { const stateStore = createChatStateStore(createChatState()); @@ -176,7 +182,7 @@ describe("ThreadResumeController", () => { await controller.resumeThread("thread"); stateStore.dispatch({ type: "active-thread/resumed", - thread: second.thread, + thread: { ...second.thread, archived: false }, cwd: "/vault", model: null, reasoningEffort: null, diff --git a/tests/features/chat/threads/thread-resume.test.ts b/tests/features/chat/threads/thread-resume.test.ts index 9afa1fe9..f1e3b9d3 100644 --- a/tests/features/chat/threads/thread-resume.test.ts +++ b/tests/features/chat/threads/thread-resume.test.ts @@ -46,7 +46,7 @@ describe("chat thread resume helpers", () => { }); }); -function responseFixture(thread: Thread): ThreadResumeResponse { +function responseFixture(thread: Thread & { archived: boolean }): ThreadResumeResponse & { thread: Thread & { archived: boolean } } { return { thread, model: "gpt-5.5", @@ -64,7 +64,7 @@ function responseFixture(thread: Thread): ThreadResumeResponse { }; } -function threadFixture(id: string, name: string): Thread { +function threadFixture(id: string, name: string): Thread & { archived: boolean } { return { id, sessionId: "session", @@ -85,6 +85,7 @@ function threadFixture(id: string, name: string): Thread { agentRole: null, gitInfo: null, name, + archived: false, turns: [], }; } diff --git a/tests/features/chat/turns/composer-submission-actions.test.ts b/tests/features/chat/turns/composer-submission-actions.test.ts index 929afb3d..231f10d5 100644 --- a/tests/features/chat/turns/composer-submission-actions.test.ts +++ b/tests/features/chat/turns/composer-submission-actions.test.ts @@ -5,7 +5,7 @@ import { createChatState, createChatStateStore } from "../../../../src/features/ import { createComposerSubmissionActions } from "../../../../src/features/chat/turns/composer-submission-actions"; import type { Thread } from "../../../../src/generated/app-server/v2/Thread"; -function thread(id: string): Thread { +function thread(id: string): Thread & { archived: boolean } { return { id, sessionId: id, @@ -26,6 +26,7 @@ function thread(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } diff --git a/tests/features/chat/turns/slash-command-actions.test.ts b/tests/features/chat/turns/slash-command-actions.test.ts index 094eec1e..d05d5e5d 100644 --- a/tests/features/chat/turns/slash-command-actions.test.ts +++ b/tests/features/chat/turns/slash-command-actions.test.ts @@ -15,7 +15,7 @@ import type { UserInput } from "../../../../src/generated/app-server/v2/UserInpu const textInput = (text: string): UserInput[] => [{ type: "text", text, text_elements: [] }]; -function thread(id: string, name: string | null = null): Thread { +function thread(id: string, name: string | null = null): Thread & { archived: boolean } { return { id, sessionId: id, @@ -36,6 +36,7 @@ function thread(id: string, name: string | null = null): Thread { agentRole: null, gitInfo: null, name, + archived: false, turns: [], }; } diff --git a/tests/features/chat/turns/slash-command-execution.test.ts b/tests/features/chat/turns/slash-command-execution.test.ts index a0937407..2992840a 100644 --- a/tests/features/chat/turns/slash-command-execution.test.ts +++ b/tests/features/chat/turns/slash-command-execution.test.ts @@ -43,7 +43,7 @@ function context(overrides: Partial = {}): SlashCo }; } -function thread(overrides: Partial = {}): Thread { +function thread(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "thread-1", sessionId: "session-1", @@ -64,9 +64,10 @@ function thread(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], ...overrides, - } as Thread; + } as Thread & { archived: boolean }; } function goal(overrides: Partial = {}): ThreadGoal { diff --git a/tests/features/chat/turns/turn-submission-controller.test.ts b/tests/features/chat/turns/turn-submission-controller.test.ts index 1a1520b4..1bbb7824 100644 --- a/tests/features/chat/turns/turn-submission-controller.test.ts +++ b/tests/features/chat/turns/turn-submission-controller.test.ts @@ -18,7 +18,7 @@ import type { UserInput } from "../../../../src/generated/app-server/v2/UserInpu const textInput = (text: string): UserInput[] => [{ type: "text", text, text_elements: [] }]; -function thread(id: string): Thread { +function thread(id: string): Thread & { archived: boolean } { return { id, sessionId: id, @@ -39,6 +39,7 @@ function thread(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], }; } diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts index 6b5c6ec6..15877689 100644 --- a/tests/features/chat/view-connection.test.ts +++ b/tests/features/chat/view-connection.test.ts @@ -5,9 +5,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_SETTINGS } from "../../../src/settings/model"; import type { CodexChatHost } from "../../../src/features/chat/view"; import { createAppServerDiagnostics } from "../../../src/app-server/compatibility"; +import { panelThreadFromAppServerThread } from "../../../src/app-server/thread-model"; import { createChatState, type ChatState } from "../../../src/features/chat/chat-state"; import { composerSlotSnapshot } from "../../../src/features/chat/panel/snapshot"; import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification"; +import type { Thread } from "../../../src/generated/app-server/v2/Thread"; import { notices } from "../../mocks/obsidian"; import { deferred, waitForAsyncWork } from "../../support/async"; import { installObsidianDomShims } from "../../support/dom"; @@ -156,7 +158,9 @@ describe("CodexChatView connection lifecycle", () => { await view.connect(); expect(refreshThreadList).toHaveBeenCalledOnce(); - expect((view as unknown as { state: { threadList: { listedThreads: unknown[] } } }).state.threadList.listedThreads).toEqual(threads); + expect((view as unknown as { state: { threadList: { listedThreads: unknown[] } } }).state.threadList.listedThreads).toEqual( + threads.map((thread) => panelThreadFromAppServerThread(thread)), + ); }); it("publishes app-server metadata after connecting", async () => { @@ -386,7 +390,7 @@ describe("CodexChatView connection lifecycle", () => { expect(client.readAccountRateLimits).toHaveBeenCalledOnce(); expect(client.listThreads).toHaveBeenCalledWith("/vault"); expect((view as unknown as { state: { threadList: { listedThreads: unknown[] } } }).state.threadList.listedThreads).toEqual([ - threadFixture("thread-1"), + panelThreadFromAppServerThread(threadFixture("thread-1")), ]); }); @@ -1062,7 +1066,7 @@ function resumedThread(threadId: string) { }; } -function threadFixture(threadId: string) { +function threadFixture(threadId: string): Thread { return { id: threadId, sessionId: "session", diff --git a/tests/features/chat/view-model.test.ts b/tests/features/chat/view-model.test.ts index dda87569..3d23daf1 100644 --- a/tests/features/chat/view-model.test.ts +++ b/tests/features/chat/view-model.test.ts @@ -237,7 +237,7 @@ function effectiveConfigFixture(config: Record): ConfigReadResp }; } -function threadFixture(id: string, name: string | null): Thread { +function threadFixture(id: string, name: string | null): Thread & { archived: boolean } { return { id, sessionId: "session", @@ -258,6 +258,7 @@ function threadFixture(id: string, name: string | null): Thread { agentRole: null, gitInfo: null, name, + archived: false, turns: [], }; } diff --git a/tests/features/thread-picker/modal.test.ts b/tests/features/thread-picker/modal.test.ts index 22fe6cf9..9b7c73e8 100644 --- a/tests/features/thread-picker/modal.test.ts +++ b/tests/features/thread-picker/modal.test.ts @@ -38,7 +38,7 @@ describe("threadOpenModeFromEvent", () => { }); }); -function thread(options: Partial & { id: string }): Thread { +function thread(options: Partial & { id: string }): Thread & { archived: boolean } { return { id: options.id, sessionId: "session", @@ -59,6 +59,7 @@ function thread(options: Partial & { id: string }): Thread { agentRole: null, gitInfo: null, name: options.name ?? null, + archived: false, turns: [], - } as Thread; + } as Thread & { archived: boolean }; } diff --git a/tests/features/threads-view/renderer.test.ts b/tests/features/threads-view/renderer.test.ts index c894d0db..f2e556c8 100644 --- a/tests/features/threads-view/renderer.test.ts +++ b/tests/features/threads-view/renderer.test.ts @@ -45,7 +45,7 @@ function openPanelSnapshot( }; } -function threadFixture(overrides: Partial = {}): Thread { +function threadFixture(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "thread", sessionId: "session", @@ -66,6 +66,7 @@ function threadFixture(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], ...overrides, }; diff --git a/tests/main.test.ts b/tests/main.test.ts index df3df1fc..238259cb 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -407,10 +407,10 @@ describe("CodexPanelPlugin boot restored panel loading", () => { it("single-flights shared thread list refreshes and caches successful results", async () => { const plugin = await pluginWithLeaves([]); - let resolveThreads!: (threads: Thread[]) => void; + let resolveThreads!: (threads: (Thread & { archived: boolean })[]) => void; const fetchThreads = vi.fn( () => - new Promise((resolve) => { + new Promise<(Thread & { archived: boolean })[]>((resolve) => { resolveThreads = resolve; }), ); @@ -570,7 +570,7 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) { ); } -function thread(id: string): Thread { +function thread(id: string): Thread & { archived: boolean } { return { id, sessionId: "session", @@ -591,8 +591,9 @@ function thread(id: string): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], - } as Thread; + } as Thread & { archived: boolean }; } function panelSnapshot( diff --git a/tests/settings/settings-tab.test.ts b/tests/settings/settings-tab.test.ts index 1ce3b63d..fd9da776 100644 --- a/tests/settings/settings-tab.test.ts +++ b/tests/settings/settings-tab.test.ts @@ -270,7 +270,7 @@ describe("settings tab", () => { }); }); -function thread(overrides: Partial = {}): Thread { +function thread(overrides: Partial = {}): Thread & { archived: boolean } { return { id: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", sessionId: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", @@ -291,6 +291,7 @@ function thread(overrides: Partial = {}): Thread { agentRole: null, gitInfo: null, name: null, + archived: false, turns: [], ...overrides, };