mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Centralize thread title projections
This commit is contained in:
parent
dd9a0b42dc
commit
80edc64443
17 changed files with 146 additions and 62 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { getThreadTitle, type Thread } from "./model";
|
||||
import type { Thread } from "./model";
|
||||
import { referencedThreadMetadataFromPrompt } from "./reference";
|
||||
import { threadUserTitle } from "./title";
|
||||
import type { ThreadTranscriptEntry } from "./transcript";
|
||||
|
||||
interface ParsedMarkdownLink {
|
||||
|
|
@ -50,7 +51,7 @@ export function archivedThreadMarkdown(
|
|||
}
|
||||
|
||||
export function archivedThreadTitle(thread: ArchiveThreadInput): string {
|
||||
return getThreadTitle(thread) || "Untitled thread";
|
||||
return threadUserTitle(thread) || "Untitled thread";
|
||||
}
|
||||
|
||||
function normalizeExportedMarkdownLinks(markdown: string, vaultPath: string): string {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getThreadTitle, type Thread } from "./model";
|
||||
import type { Thread } from "./model";
|
||||
import { threadUserTitle } from "./title";
|
||||
import type { ThreadConversationSummary } from "./transcript";
|
||||
|
||||
export const REFERENCED_THREAD_TURN_LIMIT = 20;
|
||||
|
|
@ -47,7 +48,7 @@ function referencedThreadPrompt(thread: Thread, turns: readonly ThreadConversati
|
|||
function referencedThreadMetadata(thread: Thread, count: number): ReferencedThreadMetadata {
|
||||
return {
|
||||
threadId: thread.id,
|
||||
title: getThreadTitle(thread),
|
||||
title: threadUserTitle(thread),
|
||||
includedTurns: count,
|
||||
turnLimit: REFERENCED_THREAD_TURN_LIMIT,
|
||||
};
|
||||
|
|
|
|||
43
src/domain/threads/title.ts
Normal file
43
src/domain/threads/title.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { shortThreadId } from "../../utils";
|
||||
import { getThreadTitle, type Thread } from "./model";
|
||||
|
||||
const MAX_ARCHIVED_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 threadUserTitle(thread: Thread): string {
|
||||
return usefulThreadTitle(thread) ?? getThreadTitle(thread);
|
||||
}
|
||||
|
||||
export function threadRenameDraftTitle(thread: Thread): string {
|
||||
return usefulThreadTitle(thread) ?? "";
|
||||
}
|
||||
|
||||
export function threadArchiveDisplayTitle(thread: Thread): string {
|
||||
const title = usefulThreadTitle(thread);
|
||||
return title ? truncateThreadTitle(title, MAX_ARCHIVED_THREAD_DISPLAY_TITLE_LENGTH) : "Untitled archived thread";
|
||||
}
|
||||
|
||||
export function threadWindowTitle(activeThreadId: string | null, threads: readonly Thread[], fallbackTitle?: string | null): string {
|
||||
if (!activeThreadId) return "Codex";
|
||||
|
||||
const thread = threads.find((item) => item.id === activeThreadId);
|
||||
const title = thread ? threadUserTitle(thread) : normalizeTitle(fallbackTitle) || shortThreadId(activeThreadId);
|
||||
return title ? `Codex: ${title}` : "Codex";
|
||||
}
|
||||
|
||||
function usefulThreadTitle(thread: Thread): string | null {
|
||||
for (const value of [thread.name, thread.preview]) {
|
||||
const title = normalizeTitle(value);
|
||||
if (title && title !== thread.id && !UUID_PATTERN.test(title)) return title;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTitle(value: string | null | undefined): string {
|
||||
return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
||||
}
|
||||
|
||||
function truncateThreadTitle(value: string, maxLength: number): string {
|
||||
if (value.length <= maxLength) return value;
|
||||
return `${value.slice(0, maxLength - 3).trimEnd()}...`;
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { prepareFuzzySearch, sortSearchResults, type SearchResult } from "obsidi
|
|||
import { findModelMetadataByIdOrName, sortedModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import { supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import { SLASH_COMMANDS, slashCommandSubcommands, type SlashCommandName } from "./slash-commands";
|
||||
import { getThreadTitle } from "../../../../domain/threads/model";
|
||||
import { threadUserTitle } from "../../../../domain/threads/title";
|
||||
import { shortThreadId } from "../../../../utils";
|
||||
|
||||
export interface ComposerSuggestion {
|
||||
|
|
@ -277,7 +277,7 @@ function activeThreadCommandSuggestions(beforeCursor: string, threads: readonly
|
|||
if (threads.some((thread) => thread.id.toLowerCase() === query)) return null;
|
||||
return threads
|
||||
.map((thread, index) => {
|
||||
const title = getThreadTitle(thread);
|
||||
const title = threadUserTitle(thread);
|
||||
const id = thread.id.toLowerCase();
|
||||
const normalizedTitle = title.toLowerCase();
|
||||
const score = query.length === 0 ? 2 : id.startsWith(query) ? 0 : normalizedTitle.includes(query) ? 1 : id.includes(query) ? 2 : -1;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ThreadGoal } from "../../../../domain/threads/goal";
|
|||
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { normalizeReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../../domain/threads/model";
|
||||
import { threadUserTitle } from "../../../../domain/threads/title";
|
||||
import type { ReferencedThreadMetadata } from "../../../../domain/threads/reference";
|
||||
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
|
||||
import type { GoalActions } from "../threads/goal-actions";
|
||||
|
|
@ -463,9 +463,9 @@ function resolveThreadArgument(args: string, threads: readonly Thread[]): Thread
|
|||
if (idMatches.length > 1) return { ok: false, message: `Multiple matching threads: ${idMatches.map((thread) => thread.id).join(", ")}` };
|
||||
|
||||
const titleQuery = query.toLowerCase();
|
||||
const titleMatches = threads.filter((thread) => getThreadTitle(thread).toLowerCase().includes(titleQuery));
|
||||
const titleMatches = threads.filter((thread) => threadUserTitle(thread).toLowerCase().includes(titleQuery));
|
||||
if (titleMatches.length === 1 && titleMatches[0]) return { ok: true, thread: titleMatches[0] };
|
||||
if (titleMatches.length > 1) return { ok: false, message: `Multiple matching threads: ${titleMatches.map(getThreadTitle).join(", ")}` };
|
||||
if (titleMatches.length > 1) return { ok: false, message: `Multiple matching threads: ${titleMatches.map(threadUserTitle).join(", ")}` };
|
||||
|
||||
return { ok: false, message: `No matching thread: ${query}` };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getThreadTitle } from "../../../../domain/threads/model";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import { threadRenameDraftTitle } from "../../../../domain/threads/title";
|
||||
import type { ThreadTitleContext } from "../../../../domain/threads/title-generation-model";
|
||||
import type { ThreadOperations } from "../../../threads/thread-operations";
|
||||
import type { ThreadTitleService } from "../../../threads/thread-title-service";
|
||||
|
|
@ -57,7 +57,7 @@ export function createThreadRenameEditorActions(host: ThreadRenameEditorActionsH
|
|||
start(threadId: string): void {
|
||||
const thread = threadById(host, threadId);
|
||||
if (!thread) return;
|
||||
dispatch(host, { type: "ui/rename-started", threadId, draft: getThreadTitle(thread) });
|
||||
dispatch(host, { type: "ui/rename-started", threadId, draft: threadRenameDraftTitle(thread) });
|
||||
},
|
||||
|
||||
updateDraft(threadId: string, value: string): void {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import { appServerQueryContextRawEquals, type AppServerQueryContext } from "../../../app-server/query/keys";
|
||||
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../domain/threads/model";
|
||||
import { threadUserTitle, threadWindowTitle } from "../../../domain/threads/title";
|
||||
import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
|
||||
import { shortThreadId } from "../../../utils";
|
||||
import { createChatViewDeferredTasks } from "./lifecycle";
|
||||
import { ChatResumeWorkTracker, type ChatViewDeferredTasks } from "../application/lifecycle";
|
||||
import { openPanelTurnLifecycle, parseRestoredThreadState, type ChatPanelSnapshot } from "../panel/snapshot";
|
||||
|
|
@ -16,14 +14,6 @@ import type { ChatSurfaceHandle } from "./surface-handle";
|
|||
import type { ChatPanelEnvironment } from "./runtime";
|
||||
import { createChatPanelSessionGraph, type ChatPanelSessionGraph } from "./session-graph";
|
||||
|
||||
function codexPanelDisplayTitle(activeThreadId: string | null, threads: readonly Thread[], fallbackTitle?: string | null): string {
|
||||
if (!activeThreadId) return "Codex";
|
||||
|
||||
const thread = threads.find((item) => item.id === activeThreadId);
|
||||
const title = thread ? getThreadTitle(thread).replace(/\s+/g, " ").trim() : (fallbackTitle ?? shortThreadId(activeThreadId));
|
||||
return title ? `Codex: ${title}` : "Codex";
|
||||
}
|
||||
|
||||
export class ChatPanelSession implements ChatSurfaceHandle {
|
||||
private readonly stateStore: ChatStateStore = createChatStateStore();
|
||||
private readonly graph: ChatPanelSessionGraph;
|
||||
|
|
@ -43,7 +33,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
}
|
||||
|
||||
displayTitle(): string {
|
||||
return codexPanelDisplayTitle(this.state.activeThread.id, this.state.threadList.listedThreads, this.restoredThreadTitle());
|
||||
return threadWindowTitle(this.state.activeThread.id, this.state.threadList.listedThreads, this.restoredThreadTitle());
|
||||
}
|
||||
|
||||
persistedState(): Record<string, unknown> {
|
||||
|
|
@ -105,6 +95,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
turnLifecycle: openPanelTurnLifecycle(this.state.turn.lifecycle),
|
||||
pendingApprovals: this.state.requests.approvals.length,
|
||||
pendingUserInputs: this.state.requests.pendingUserInputs.length,
|
||||
pendingMcpElicitations: this.state.requests.pendingMcpElicitations.length,
|
||||
hasComposerDraft: this.state.composer.draft.trim().length > 0,
|
||||
connected: this.graph.connection.manager.isConnected(),
|
||||
};
|
||||
|
|
@ -234,7 +225,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
const threadId = this.state.activeThread.id;
|
||||
if (!threadId) return null;
|
||||
const thread = this.state.threadList.listedThreads.find((item) => item.id === threadId);
|
||||
return thread ? getThreadTitle(thread) : null;
|
||||
return thread ? threadUserTitle(thread) : null;
|
||||
}
|
||||
|
||||
private restoredThreadTitle(): string | null {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ComponentChild as UiNode } from "preact";
|
|||
import { h } from "preact";
|
||||
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../../domain/threads/model";
|
||||
import { threadUserTitle } from "../../../../domain/threads/title";
|
||||
import { rateLimitSummary } from "../../presentation/runtime/status";
|
||||
import { connectionDiagnosticSectionsModel } from "../../application/connection/diagnostics-display";
|
||||
import { toolInventoryDiagnosticSections } from "../../application/connection/tool-inventory-display";
|
||||
|
|
@ -135,7 +135,7 @@ function toolbarThreadRows(input: {
|
|||
return input.threads.map((thread) => {
|
||||
const threadId = thread.id;
|
||||
return {
|
||||
title: getThreadTitle(thread),
|
||||
title: threadUserTitle(thread),
|
||||
threadId,
|
||||
selected: threadId === input.activeThreadId,
|
||||
disabled: input.turnBusy && threadId !== input.activeThreadId,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Notice, Platform, SuggestModal, type App } from "obsidian";
|
||||
|
||||
import { getThreadTitle } from "../../domain/threads/model";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import { threadUserTitle } from "../../domain/threads/title";
|
||||
import { shortThreadId } from "../../utils";
|
||||
import type { ThreadCatalogActiveReader } from "../../workspace/thread-catalog";
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ function threadPickerSuggestions(threads: readonly Thread[], queryText: string):
|
|||
return [...threads]
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
.map((thread, index) => {
|
||||
const title = getThreadTitle(thread);
|
||||
const title = threadUserTitle(thread);
|
||||
const id = thread.id.toLowerCase();
|
||||
const normalizedTitle = title.toLowerCase();
|
||||
const shortId = shortThreadId(thread.id).toLowerCase();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import { explicitThreadName, getThreadTitle } from "../../domain/threads/model";
|
||||
import { threadRenameDraftTitle, threadUserTitle } from "../../domain/threads/title";
|
||||
import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator";
|
||||
|
||||
type ThreadsLiveStatus = "needs-input" | "approval" | "running" | "draft" | "offline" | "open";
|
||||
|
||||
|
|
@ -69,12 +69,12 @@ export function threadRows(
|
|||
const rename = renameStates.get(thread.id);
|
||||
return {
|
||||
thread,
|
||||
title: getThreadTitle(thread),
|
||||
title: threadUserTitle(thread),
|
||||
live,
|
||||
selected,
|
||||
rename: {
|
||||
active: rename !== undefined,
|
||||
draft: rename?.draft ?? explicitThreadName(thread) ?? getThreadTitle(thread),
|
||||
draft: rename?.draft ?? threadRenameDraftTitle(thread),
|
||||
generating: rename?.kind === "generating",
|
||||
},
|
||||
archiveConfirm: {
|
||||
|
|
@ -213,7 +213,7 @@ function snapshotsForThreads(snapshots: OpenCodexPanelSnapshot[]): Map<string, O
|
|||
}
|
||||
|
||||
function snapshotStatus(snapshot: OpenCodexPanelSnapshot): ThreadsLiveStatus {
|
||||
if (snapshot.pendingUserInputs > 0) return "needs-input";
|
||||
if (snapshot.pendingUserInputs > 0 || snapshot.pendingMcpElicitations > 0) return "needs-input";
|
||||
if (snapshot.pendingApprovals > 0) return "approval";
|
||||
if (snapshot.turnLifecycle.kind !== "idle") return "running";
|
||||
if (snapshot.hasComposerDraft) return "draft";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import { threadArchiveDisplayTitle } from "../domain/threads/title";
|
||||
import { shortThreadId } from "../utils";
|
||||
import { archivedThreadDisplayTitle } from "./archived-thread-title";
|
||||
import type { ArchivedThreadSectionState } from "./section-state";
|
||||
import {
|
||||
SettingRow,
|
||||
|
|
@ -89,7 +89,7 @@ function ArchivedThreadList({ state }: { state: ArchivedThreadSectionState }): U
|
|||
}
|
||||
|
||||
function ArchivedThreadRow({ thread, state }: { thread: Thread; state: ArchivedThreadSectionState }): UiNode {
|
||||
const title = archivedThreadDisplayTitle(thread);
|
||||
const title = threadArchiveDisplayTitle(thread);
|
||||
const deleteConfirming = state.deleteConfirmThreadId === thread.id;
|
||||
return (
|
||||
<SettingRow
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { getThreadTitle, type Thread } from "../domain/threads/model";
|
||||
|
||||
const MAX_ARCHIVED_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 archivedThreadDisplayTitle(thread: Thread): string {
|
||||
const title = normalizedThreadTitle(thread);
|
||||
if (!title || title === thread.id || UUID_PATTERN.test(title)) return "Untitled archived thread";
|
||||
return truncateTitle(title, MAX_ARCHIVED_THREAD_DISPLAY_TITLE_LENGTH);
|
||||
}
|
||||
|
||||
function normalizedThreadTitle(thread: Thread): string {
|
||||
return getThreadTitle(thread).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function truncateTitle(value: string, maxLength: number): string {
|
||||
if (value.length <= maxLength) return value;
|
||||
return `${value.slice(0, maxLength - 3).trimEnd()}...`;
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import { restoreArchivedThread as restoreArchivedThreadOnAppServer } from "../ap
|
|||
import type { HookItem, ModelMetadata, ReasoningEffort } from "../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, sortedModelMetadata, supportedEffortsForModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import { threadArchiveDisplayTitle } from "../domain/threads/title";
|
||||
import { errorMessage } from "../utils";
|
||||
import { archivedThreadDisplayTitle } from "./archived-thread-title";
|
||||
import { loadHookData } from "./app-server-data";
|
||||
import type { SettingsDynamicDataHost } from "./host";
|
||||
import {
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from "./lifecycle";
|
||||
|
||||
function archivedThreadTitleForStatus(thread: Thread | undefined, threadId: string): string {
|
||||
return thread ? archivedThreadDisplayTitle(thread) : threadId;
|
||||
return thread ? threadArchiveDisplayTitle(thread) : threadId;
|
||||
}
|
||||
|
||||
interface SettingsDynamicDataControllerCallbacks {
|
||||
|
|
@ -355,7 +355,7 @@ export class SettingsDynamicDataController {
|
|||
this.host.threadCatalog.recordThreadRestored(restoredThread);
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "loaded",
|
||||
status: `Restored "${archivedThreadDisplayTitle(restoredThread)}".`,
|
||||
status: `Restored "${threadArchiveDisplayTitle(restoredThread)}".`,
|
||||
operationToken,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
upsertThread,
|
||||
type Thread,
|
||||
} from "../../../src/domain/threads/model";
|
||||
import { threadArchiveDisplayTitle, threadRenameDraftTitle, threadUserTitle, threadWindowTitle } from "../../../src/domain/threads/title";
|
||||
|
||||
describe("thread helpers", () => {
|
||||
it("resolves display titles from explicit names, previews, then ids", () => {
|
||||
|
|
@ -16,6 +17,35 @@ describe("thread helpers", () => {
|
|||
expect(getThreadTitle(thread({ id: "thread-id", name: null, preview: "" }))).toBe("thread-id");
|
||||
});
|
||||
|
||||
it("keeps user-facing titles identifiable while keeping rename drafts human-authored", () => {
|
||||
const idOnly = thread({ id: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", name: null, preview: "" });
|
||||
|
||||
expect(threadUserTitle(idOnly)).toBe("019e0182-cb70-7a72-ab48-8bc9d0b0d781");
|
||||
expect(threadRenameDraftTitle(idOnly)).toBe("");
|
||||
expect(threadArchiveDisplayTitle(idOnly)).toBe("Untitled archived thread");
|
||||
});
|
||||
|
||||
it("uses useful preview text instead of UUID-like names for draft and archive titles", () => {
|
||||
const uuidNamed = thread({
|
||||
id: "thread-id",
|
||||
name: "019e0182-cb70-7a72-ab48-8bc9d0b0d781",
|
||||
preview: " Useful preview ",
|
||||
});
|
||||
|
||||
expect(threadUserTitle(uuidNamed)).toBe("Useful preview");
|
||||
expect(threadRenameDraftTitle(uuidNamed)).toBe("Useful preview");
|
||||
expect(threadArchiveDisplayTitle(uuidNamed)).toBe("Useful preview");
|
||||
});
|
||||
|
||||
it("builds window titles from loaded threads, restored titles, then short ids", () => {
|
||||
const uuid = "019e0182-cb70-7a72-ab48-8bc9d0b0d781";
|
||||
|
||||
expect(threadWindowTitle(null, [])).toBe("Codex");
|
||||
expect(threadWindowTitle("thread", [thread({ id: "thread", name: " Named thread " })])).toBe("Codex: Named thread");
|
||||
expect(threadWindowTitle("thread", [], " Restored title ")).toBe("Codex: Restored title");
|
||||
expect(threadWindowTitle(uuid, [], null)).toBe("Codex: 019e0182");
|
||||
});
|
||||
|
||||
it("inherits only explicit thread names for forked threads", () => {
|
||||
expect(inheritedForkThreadName("named", [thread({ id: "named", name: "親スレッド", preview: "Preview" })])).toBe("親スレッド");
|
||||
expect(inheritedForkThreadName("preview-only", [thread({ id: "preview-only", preview: "Preview" })])).toBeNull();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ describe("ThreadRenameEditorActions", () => {
|
|||
expect(actions.editState("thread")).toEqual({ draft: "New name", generating: false });
|
||||
});
|
||||
|
||||
it("starts rename drafts from useful titles instead of id fallbacks", () => {
|
||||
const { actions, stateStore } = actionsFixture();
|
||||
stateStore.dispatch({ type: "thread-list/applied", threads: [{ ...threadFixture("thread"), preview: "" }] });
|
||||
|
||||
actions.start("thread");
|
||||
|
||||
expect(actions.editState("thread")).toEqual({ draft: "", generating: false });
|
||||
});
|
||||
|
||||
it("clears inline rename state through chat UI state", () => {
|
||||
const { actions } = actionsFixture();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import type { Thread } from "../../../src/domain/threads/model";
|
||||
import { threadRows, transitionThreadsRenameState, type ThreadsRenameState } from "../../../src/features/threads-view/state";
|
||||
import type { OpenCodexPanelSnapshot } from "../../../src/workspace/panel-coordinator";
|
||||
|
||||
describe("threads view rename state", () => {
|
||||
it("keeps a late auto-name result from reviving a cancelled rename", () => {
|
||||
|
|
@ -74,6 +75,13 @@ describe("threads view rename state", () => {
|
|||
it("initializes rename drafts from normalized explicit thread names", () => {
|
||||
expect(threadRows([thread({ name: " Saved name ", preview: "Preview" })], [], new Map())[0]?.rename.draft).toBe("Saved name");
|
||||
expect(threadRows([thread({ name: " ", preview: "Preview title" })], [], new Map())[0]?.rename.draft).toBe("Preview title");
|
||||
expect(threadRows([thread({ name: null, preview: "" })], [], new Map())[0]?.rename.draft).toBe("");
|
||||
});
|
||||
|
||||
it("treats pending MCP elicitations as user input live state", () => {
|
||||
const rows = threadRows([thread()], [openPanelSnapshot({ pendingMcpElicitations: 1 })], new Map());
|
||||
|
||||
expect(rows[0]?.live).toMatchObject({ status: "needs-input", label: "Needs input" });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -104,3 +112,18 @@ function thread(overrides: Partial<Thread> = {}): Thread {
|
|||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function openPanelSnapshot(overrides: Partial<OpenCodexPanelSnapshot> = {}): OpenCodexPanelSnapshot {
|
||||
return {
|
||||
viewId: "view",
|
||||
threadId: "thread",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
lastFocused: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { SettingsDynamicDataController, type SettingsDynamicDataSnapshot } from
|
|||
import { CodexPanelSettingTab } from "../../src/settings/tab";
|
||||
import type { CodexPanelSettingTabHost } from "../../src/settings/host";
|
||||
import type { Thread } from "../../src/domain/threads/model";
|
||||
import { archivedThreadDisplayTitle } from "../../src/settings/archived-thread-title";
|
||||
import { threadArchiveDisplayTitle } from "../../src/domain/threads/title";
|
||||
import { notices } from "../mocks/obsidian";
|
||||
import { deferred } from "../support/async";
|
||||
import { installObsidianDomShims } from "../support/dom";
|
||||
|
|
@ -33,14 +33,19 @@ describe("settings tab", () => {
|
|||
});
|
||||
|
||||
it("uses a placeholder for threads without a useful title", () => {
|
||||
expect(archivedThreadDisplayTitle(panelThread({ name: null, preview: "" }))).toBe("Untitled archived thread");
|
||||
expect(archivedThreadDisplayTitle(panelThread({ name: "019e0182-cb70-7a72-ab48-8bc9d0b0d781" }))).toBe("Untitled archived thread");
|
||||
expect(threadArchiveDisplayTitle(panelThread({ name: null, preview: "" }))).toBe("Untitled archived thread");
|
||||
expect(threadArchiveDisplayTitle(panelThread({ name: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", preview: "" }))).toBe(
|
||||
"Untitled archived thread",
|
||||
);
|
||||
expect(threadArchiveDisplayTitle(panelThread({ name: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", preview: "Preview title" }))).toBe(
|
||||
"Preview title",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes and truncates archived thread titles", () => {
|
||||
expect(archivedThreadDisplayTitle(panelThread({ preview: "A title\nwith extra\tspace" }))).toBe("A title with extra space");
|
||||
expect(threadArchiveDisplayTitle(panelThread({ preview: "A title\nwith extra\tspace" }))).toBe("A title with extra space");
|
||||
|
||||
const title = archivedThreadDisplayTitle(panelThread({ preview: "x".repeat(120) }));
|
||||
const title = threadArchiveDisplayTitle(panelThread({ preview: "x".repeat(120) }));
|
||||
expect(title).toHaveLength(96);
|
||||
expect(title.endsWith("...")).toBe(true);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue