Constrain generated Thread model to app-server boundary

This commit is contained in:
murashit 2026-06-09 10:55:06 +09:00
parent 6598e62a14
commit 3029bc478d
54 changed files with 258 additions and 175 deletions

View file

@ -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<T> = { kind: "unloaded" } | { kind: "loaded"; data: T };
export interface SharedAppServerState {
threads: SharedCache<readonly Thread[]>;
threads: SharedCache<readonly PanelThread[]>;
appServerMetadata: SharedCache<SharedAppServerMetadata>;
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[] {

View file

@ -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<readonly Thread[]> };
type ThreadListRefreshLifecycleState = { kind: "idle" } | { kind: "refreshing"; promise: Promise<readonly PanelThread[]> };
export class SharedAppServerCache {
private state: SharedAppServerState = createSharedAppServerState();
private threadListRefreshLifecycle: ThreadListRefreshLifecycleState = { kind: "idle" };
refreshThreadList(
fetchThreads: () => Promise<readonly Thread[]>,
onSnapshot?: (threads: readonly Thread[]) => void,
): Promise<readonly Thread[]> {
fetchThreads: () => Promise<readonly PanelThread[]>,
onSnapshot?: (threads: readonly PanelThread[]) => void,
): Promise<readonly PanelThread[]> {
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);
}

View file

@ -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;
}

View file

@ -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<ArchiveExportSettings>): string {
export function markdownFromThread(thread: ArchiveThreadInput, exportedAt = new Date(), settings?: Partial<ArchiveExportSettings>): 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";
}

View file

@ -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));
}

View file

@ -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[],

View file

@ -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<readonly Thread[]>;
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<Awaited<ReturnType<AppServerClient["startThread"]>> | null>;
applyThreadList: (threads: readonly PanelThread[]) => void;
loadThreadList: () => Promise<readonly PanelThread[]>;
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<StartedThreadSummary | null>;
}
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<readonly Thread[]> {
async function loadThreadList(host: ChatAppServerThreadActionsHost): Promise<readonly PanelThread[]> {
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<Awaited<ReturnType<AppServerClient["startThread"]>> | null> {
): Promise<StartedThreadSummary | null> {
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 };
}

View file

@ -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<readonly Thread[]>): Promise<readonly Thread[]>;
cachedThreadList(): readonly Thread[] | null;
applyThreadListSnapshot(threads: readonly PanelThread[]): void;
refreshThreadList(fetchThreads: () => Promise<readonly PanelThread[]>): Promise<readonly PanelThread[]>;
cachedThreadList(): readonly PanelThread[] | null;
publishAppServerMetadata(metadata: SharedAppServerMetadata): void;
cachedAppServerMetadata(): SharedAppServerMetadata | null;
}

View file

@ -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 }) };
}

View file

@ -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;
}

View file

@ -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,
};

View file

@ -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;

View file

@ -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)" },
],
},
{

View file

@ -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,
});
}

View file

@ -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");
}

View file

@ -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",

View file

@ -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);
}
}

View file

@ -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<ReturnType<AppServerClient["resumeThread"]>>): 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

View file

@ -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[];
}

View file

@ -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),

View file

@ -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<ThreadReferenceInput | null> {
try {

View file

@ -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<void>;
startThreadForGoal: (objective: string) => Promise<string | null>;
resumeThread: (threadId: string) => Promise<void>;
referThread: (thread: Thread, message: string) => Promise<ThreadReferenceInput | null>;
referThread: (thread: PanelThread, message: string) => Promise<ThreadReferenceInput | null>;
forkThread: (threadId: string) => Promise<void>;
rollbackThread: (threadId: string) => Promise<void>;
compactThread: (threadId: string) => Promise<void>;
@ -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);

View file

@ -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();

View file

@ -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<readonly Thread[]>): Promise<readonly Thread[]>;
cachedThreadList(): readonly PanelThread[] | null;
refreshThreadList(fetchThreads: () => Promise<readonly PanelThread[]>): Promise<readonly PanelThread[]>;
openThreadInCurrentView(threadId: string): Promise<void>;
openThreadInAvailableView(threadId: string): Promise<void>;
}
interface ThreadSuggestion {
thread: Thread;
thread: PanelThread;
title: string;
}
@ -41,7 +42,7 @@ export async function openThreadPicker(host: ThreadPickerHost): Promise<void> {
}
}
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<readonly Thread[]> {
async function loadThreadPickerThreads(host: ThreadPickerHost): Promise<readonly PanelThread[]> {
const cached = host.cachedThreadList();
if (cached) return cached;
@ -99,7 +100,7 @@ async function loadThreadPickerThreads(host: ThreadPickerHost): Promise<readonly
if (!client) throw new Error("Codex app-server is not connected.");
return await host.refreshThreadList(async () => {
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<readonly
class ThreadPickerModal extends SuggestModal<ThreadSuggestion> {
constructor(
private readonly host: ThreadPickerHost,
private readonly threads: readonly Thread[],
private readonly threads: readonly PanelThread[],
) {
super(host.app);
this.limit = MAX_THREAD_PICKER_SUGGESTIONS;

View file

@ -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<ThreadsLiveStatus, number> = {
};
export function threadRows(
threads: readonly Thread[],
threads: readonly PanelThread[],
snapshots: OpenCodexPanelSnapshot[],
renameStates: ReadonlyMap<string, ThreadsRenameState>,
archiveConfirmThreadId: string | null = null,

View file

@ -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<readonly Thread[]>): Promise<readonly Thread[]>;
cachedThreadList(): readonly Thread[] | null;
refreshThreadList(fetchThreads: () => Promise<readonly PanelThread[]>): Promise<readonly PanelThread[]>;
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<string, ThreadsRenameState>();
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,
);

View file

@ -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<Model[]>;
hooks: SettledSettingsData<LoadedHooks>;
archivedThreads: SettledSettingsData<Thread[]>;
archivedThreads: SettledSettingsData<PanelThread[]>;
}
type SettledSettingsData<T> =
@ -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)}` },

View file

@ -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;

View file

@ -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();

View file

@ -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);
}

View file

@ -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: [],
};
}

View file

@ -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> = {}): Thread {
function thread(overrides: Partial<Thread & { archived: boolean }> = {}): 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> = {}): Thread {
agentRole: null,
gitInfo: null,
name: null,
archived: false,
turns: [],
...overrides,
};

View file

@ -9,7 +9,7 @@ import {
referencedThreadTurns,
} from "../../../src/domain/threads/reference";
function thread(overrides: Partial<Thread> = {}): Thread {
function thread(overrides: Partial<Thread & { archived: boolean }> = {}): Thread & { archived: boolean } {
return {
id: "019abcde-0000-7000-8000-000000000001",
sessionId: "session-1",
@ -30,9 +30,10 @@ function thread(overrides: Partial<Thread> = {}): 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 {

View file

@ -48,7 +48,7 @@ describe("thread helpers", () => {
});
});
function thread(overrides: Partial<Thread> = {}): Thread {
function thread(overrides: Partial<Thread & { archived: boolean }> = {}): Thread & { archived: boolean } {
return {
id: "thread-1",
sessionId: "session-1",
@ -69,7 +69,8 @@ function thread(overrides: Partial<Thread> = {}): Thread {
agentRole: null,
gitInfo: null,
name: null,
archived: false,
turns: [],
...overrides,
} as Thread;
} as Thread & { archived: boolean };
}

View file

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

View file

@ -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: [],
};
}

View file

@ -19,7 +19,7 @@ function expectPresent<T>(value: T | null | undefined): T {
return value;
}
function thread(overrides: Partial<Thread> = {}): Thread {
function thread(overrides: Partial<Thread & { archived: boolean }> = {}): Thread & { archived: boolean } {
return {
id: "019abcde-0000-7000-8000-000000000001",
sessionId: "session-1",
@ -40,9 +40,10 @@ function thread(overrides: Partial<Thread> = {}): 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> = {}): Model {

View file

@ -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",

View file

@ -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: "次の回答" },

View file

@ -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: [],
};
}

View file

@ -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: [],
};
}

View file

@ -147,7 +147,7 @@ function fakeClient(options: { setThreadName?: ReturnType<typeof vi.fn> } = {}):
} 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: [],
};
}

View file

@ -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<ConstructorParameters<typeof ThreadResumeController>[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,

View file

@ -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: [],
};
}

View file

@ -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: [],
};
}

View file

@ -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: [],
};
}

View file

@ -43,7 +43,7 @@ function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCo
};
}
function thread(overrides: Partial<Thread> = {}): Thread {
function thread(overrides: Partial<Thread & { archived: boolean }> = {}): Thread & { archived: boolean } {
return {
id: "thread-1",
sessionId: "session-1",
@ -64,9 +64,10 @@ function thread(overrides: Partial<Thread> = {}): Thread {
agentRole: null,
gitInfo: null,
name: null,
archived: false,
turns: [],
...overrides,
} as Thread;
} as Thread & { archived: boolean };
}
function goal(overrides: Partial<ThreadGoal> = {}): ThreadGoal {

View file

@ -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: [],
};
}

View file

@ -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",

View file

@ -237,7 +237,7 @@ function effectiveConfigFixture(config: Record<string, unknown>): 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: [],
};
}

View file

@ -38,7 +38,7 @@ describe("threadOpenModeFromEvent", () => {
});
});
function thread(options: Partial<Thread> & { id: string }): Thread {
function thread(options: Partial<Thread & { archived: boolean }> & { id: string }): Thread & { archived: boolean } {
return {
id: options.id,
sessionId: "session",
@ -59,6 +59,7 @@ function thread(options: Partial<Thread> & { id: string }): Thread {
agentRole: null,
gitInfo: null,
name: options.name ?? null,
archived: false,
turns: [],
} as Thread;
} as Thread & { archived: boolean };
}

View file

@ -45,7 +45,7 @@ function openPanelSnapshot(
};
}
function threadFixture(overrides: Partial<Thread> = {}): Thread {
function threadFixture(overrides: Partial<Thread & { archived: boolean }> = {}): Thread & { archived: boolean } {
return {
id: "thread",
sessionId: "session",
@ -66,6 +66,7 @@ function threadFixture(overrides: Partial<Thread> = {}): Thread {
agentRole: null,
gitInfo: null,
name: null,
archived: false,
turns: [],
...overrides,
};

View file

@ -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<Thread[]>((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(

View file

@ -270,7 +270,7 @@ describe("settings tab", () => {
});
});
function thread(overrides: Partial<Thread> = {}): Thread {
function thread(overrides: Partial<Thread & { archived: boolean }> = {}): 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> = {}): Thread {
agentRole: null,
gitInfo: null,
name: null,
archived: false,
turns: [],
...overrides,
};