From 2918451eaf004e981707a63e608933bb3c3205d2 Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 23 May 2026 05:01:29 +0900 Subject: [PATCH] Finalize strict ESLint fixes --- src/panel/controller.ts | 14 +-- src/panel/thread-naming.ts | 9 +- src/panel/view.ts | 1 + src/settings/dynamic-sections.ts | 8 +- src/ui/message-stream.ts | 9 ++ src/ui/toolbar.ts | 14 ++- tests/approvals/approvals.test.ts | 150 +++++++++++++++------------- tests/mocks/obsidian.ts | 6 ++ tests/user-input/user-input.test.ts | 40 ++++---- 9 files changed, 145 insertions(+), 106 deletions(-) diff --git a/src/panel/controller.ts b/src/panel/controller.ts index 9a7803f8..ebbb1689 100644 --- a/src/panel/controller.ts +++ b/src/panel/controller.ts @@ -201,13 +201,7 @@ export class PanelController { } else if (method === "hook/completed") { this.upsertHookRun(params.run, params.turnId, params.run.status); } else if (method === "item/mcpToolCall/progress") { - this.state.displayItems = appendToolOutput( - this.state.displayItems, - params.itemId, - params.turnId, - params.message, - "mcp progress", - ); + this.state.displayItems = appendToolOutput(this.state.displayItems, params.itemId, params.turnId, params.message, "mcp progress"); } else if (method === "item/autoApprovalReview/started" || method === "item/autoApprovalReview/completed") { const reviewItem = createAutoReviewResultItem(params); this.state.displayItems = upsertDisplayItem(removeUnstructuredAutoReviewWarnings(this.state.displayItems), reviewItem); @@ -427,6 +421,7 @@ export class PanelController { this.state.activeReasoningEffort = settings.effort; this.state.activeCollaborationMode = settings.collaborationMode.mode; this.state.requestedCollaborationMode = settings.collaborationMode.mode; + const nullableSettings: ThreadSettingsWithNullableReviewer = settings; const serviceTierOverride = this.state.activeServiceTierOverride || this.state.requestedServiceTier !== null; const approvalsReviewerOverride = this.state.activeApprovalsReviewerOverride || this.state.requestedApprovalsReviewer !== null; this.state.activeServiceTier = settings.serviceTier ?? (serviceTierOverride ? this.state.activeServiceTier : null); @@ -437,6 +432,11 @@ export class PanelController { } } +type ThreadSettings = Extract["params"]["threadSettings"]; +type ThreadSettingsWithNullableReviewer = Omit & { + approvalsReviewer: ThreadSettings["approvalsReviewer"] | null; +}; + function removeUnstructuredAutoReviewWarnings(items: DisplayItem[]): DisplayItem[] { return items.filter((item) => !isUnstructuredAutoReviewWarning(item)); } diff --git a/src/panel/thread-naming.ts b/src/panel/thread-naming.ts index b709cdd5..019c5342 100644 --- a/src/panel/thread-naming.ts +++ b/src/panel/thread-naming.ts @@ -105,9 +105,12 @@ export async function generateThreadTitleWithCodex( let client!: AppServerClient; client = new AppServerClient(codexPath, cwd, { - onNotification: (notification) => { handleNamingNotification(notification); }, - onServerRequest: (request) => - { client.rejectServerRequest(request.id, -32601, "Thread title generation does not handle server requests."); }, + onNotification: (notification) => { + handleNamingNotification(notification); + }, + onServerRequest: (request) => { + client.rejectServerRequest(request.id, -32601, "Thread title generation does not handle server requests."); + }, onLog: () => undefined, onExit: () => { if (completed) return; diff --git a/src/panel/view.ts b/src/panel/view.ts index ddeebdba..3b32b66d 100644 --- a/src/panel/view.ts +++ b/src/panel/view.ts @@ -930,6 +930,7 @@ export class CodexPanelView extends ItemView { this.render(); await this.ensureConnected(); + if (!threadId) return; try { await this.resumeThread(threadId); } catch (error) { diff --git a/src/settings/dynamic-sections.ts b/src/settings/dynamic-sections.ts index 5b0b3f24..32f75c8f 100644 --- a/src/settings/dynamic-sections.ts +++ b/src/settings/dynamic-sections.ts @@ -212,8 +212,12 @@ function renderHookRow(list: HTMLElement, hook: HookMetadata, state: HookSection }); } -function firstNonEmptyString(...values: string[]): string { - return values.find((value) => value.length > 0) ?? values.at(-1) ?? ""; +function firstNonEmptyString(...values: (string | null | undefined)[]): string { + return ( + values.find((value): value is string => typeof value === "string" && value.length > 0) ?? + values.find((value): value is string => typeof value === "string") ?? + "" + ); } function formatThreadDate(timestamp: number): string { diff --git a/src/ui/message-stream.ts b/src/ui/message-stream.ts index 3e8f995e..62fc27ce 100644 --- a/src/ui/message-stream.ts +++ b/src/ui/message-stream.ts @@ -48,6 +48,12 @@ export interface MessageStreamContext { renderPendingRequests?: () => HTMLElement | null; } +type RenderableMessageItem = Extract; + +function isRenderableMessageItem(item: DisplayItem): item is RenderableMessageItem { + return item.kind === "message" || item.kind === "system" || item.kind === "userInputResult"; +} + export function messageRenderBlocks(context: MessageStreamContext): MessageRenderBlock[] { const blocks: MessageRenderBlock[] = []; @@ -212,6 +218,9 @@ function renderDisplayItem(parent: HTMLElement, item: DisplayItem, context: Mess renderToolResult(parent, item, context); return; } + if (!isRenderableMessageItem(item)) { + return; + } const messageEl = parent.createDiv({ cls: messageClass(item) }); applyExecutionStateClass(messageEl, executionState(item)); const role = messageEl.createDiv({ cls: "codex-panel__message-role" }); diff --git a/src/ui/toolbar.ts b/src/ui/toolbar.ts index aa508b9b..e882ddf0 100644 --- a/src/ui/toolbar.ts +++ b/src/ui/toolbar.ts @@ -107,12 +107,16 @@ export function toolbarSignature(model: ToolbarViewModel): string { openPanel: model.openPanel, threads: model.threads.map( (thread) => - `${thread.threadId}:${thread.title}:${String(thread.selected)}:${String(thread.disabled)}:${String(thread.canArchive)}:${thread.rename?.draft ?? ""}:${ - String(thread.rename?.generating ?? false) - }`, + `${thread.threadId}:${thread.title}:${String(thread.selected)}:${String(thread.disabled)}:${String(thread.canArchive)}:${thread.rename?.draft ?? ""}:${String( + thread.rename?.generating ?? false, + )}`, + ), + modelChoices: model.modelChoices.map( + (choice) => `${choice.label}:${String(choice.selected)}:${String(choice.disabled)}:${choice.meta ?? ""}`, + ), + effortChoices: model.effortChoices.map( + (choice) => `${choice.label}:${String(choice.selected)}:${String(choice.disabled)}:${choice.meta ?? ""}`, ), - modelChoices: model.modelChoices.map((choice) => `${choice.label}:${String(choice.selected)}:${String(choice.disabled)}:${choice.meta ?? ""}`), - effortChoices: model.effortChoices.map((choice) => `${choice.label}:${String(choice.selected)}:${String(choice.disabled)}:${choice.meta ?? ""}`), connectLabel: model.connectLabel, diagnostics: model.diagnostics.map((section) => ({ title: section.title, diff --git a/tests/approvals/approvals.test.ts b/tests/approvals/approvals.test.ts index 759994a1..26141f8d 100644 --- a/tests/approvals/approvals.test.ts +++ b/tests/approvals/approvals.test.ts @@ -95,47 +95,53 @@ describe("approval model", () => { }); it("shows approval reasons first in pending request summaries", () => { - const command = expectPresent(toPendingApproval({ - id: 20, - method: "item/commandExecution/requestApproval", - params: { - command: "npm run build", - cwd: "/tmp/project", - threadId: "thread", - turnId: "turn", - itemId: "command", - startedAtMs: 1, - reason: "Needs unsandboxed access", - commandActions: [], - proposedExecpolicyAmendment: null, - proposedNetworkPolicyAmendments: [], - }, - })); - const fileChange = expectPresent(toPendingApproval({ - id: 21, - method: "item/fileChange/requestApproval", - params: { - threadId: "thread", - turnId: "turn", - itemId: "file", - startedAtMs: 1, - reason: "Write outside workspace", - grantRoot: "/tmp/project", - }, - })); - const permissions = expectPresent(toPendingApproval({ - id: 22, - method: "item/permissions/requestApproval", - params: { - cwd: "/tmp/project", - threadId: "thread", - turnId: "turn", - itemId: "permissions", - startedAtMs: 1, - reason: "Need network", - permissions: { network: { enabled: true }, fileSystem: null }, - }, - })); + const command = expectPresent( + toPendingApproval({ + id: 20, + method: "item/commandExecution/requestApproval", + params: { + command: "npm run build", + cwd: "/tmp/project", + threadId: "thread", + turnId: "turn", + itemId: "command", + startedAtMs: 1, + reason: "Needs unsandboxed access", + commandActions: [], + proposedExecpolicyAmendment: null, + proposedNetworkPolicyAmendments: [], + }, + }), + ); + const fileChange = expectPresent( + toPendingApproval({ + id: 21, + method: "item/fileChange/requestApproval", + params: { + threadId: "thread", + turnId: "turn", + itemId: "file", + startedAtMs: 1, + reason: "Write outside workspace", + grantRoot: "/tmp/project", + }, + }), + ); + const permissions = expectPresent( + toPendingApproval({ + id: 22, + method: "item/permissions/requestApproval", + params: { + cwd: "/tmp/project", + threadId: "thread", + turnId: "turn", + itemId: "permissions", + startedAtMs: 1, + reason: "Need network", + permissions: { network: { enabled: true }, fileSystem: null }, + }, + }), + ); expect(approvalSummary(command)).toBe("Needs unsandboxed access\nnpm run build"); expect(approvalSummary(fileChange)).toBe("Write outside workspace\ngrant root: /tmp/project"); @@ -143,19 +149,21 @@ describe("approval model", () => { }); it("keeps approval details semantic and omits raw payloads", () => { - const approval = expectPresent(toPendingApproval({ - id: 23, - method: "item/permissions/requestApproval", - params: { - cwd: "/tmp/project", - threadId: "thread", - turnId: "turn", - itemId: "permissions", - startedAtMs: 1, - reason: "Need network", - permissions: { network: { enabled: true }, fileSystem: null }, - }, - })); + const approval = expectPresent( + toPendingApproval({ + id: 23, + method: "item/permissions/requestApproval", + params: { + cwd: "/tmp/project", + threadId: "thread", + turnId: "turn", + itemId: "permissions", + startedAtMs: 1, + reason: "Need network", + permissions: { network: { enabled: true }, fileSystem: null }, + }, + }), + ); expect(approvalDetails(approval)).toEqual([ { key: "reason", value: "Need network" }, @@ -165,22 +173,24 @@ describe("approval model", () => { }); it("omits empty approval detail rows", () => { - const approval = expectPresent(toPendingApproval({ - id: 24, - method: "item/commandExecution/requestApproval", - params: { - command: "npm test", - cwd: "/tmp/project", - threadId: "thread", - turnId: "turn", - itemId: "command", - startedAtMs: 1, - reason: null, - commandActions: [], - proposedExecpolicyAmendment: null, - proposedNetworkPolicyAmendments: [], - }, - })); + const approval = expectPresent( + toPendingApproval({ + id: 24, + method: "item/commandExecution/requestApproval", + params: { + command: "npm test", + cwd: "/tmp/project", + threadId: "thread", + turnId: "turn", + itemId: "command", + startedAtMs: 1, + reason: null, + commandActions: [], + proposedExecpolicyAmendment: null, + proposedNetworkPolicyAmendments: [], + }, + }), + ); expect(approvalDetails(approval)).toEqual([ { key: "command", value: "npm test" }, diff --git a/tests/mocks/obsidian.ts b/tests/mocks/obsidian.ts index fa65928a..03110dc9 100644 --- a/tests/mocks/obsidian.ts +++ b/tests/mocks/obsidian.ts @@ -310,6 +310,12 @@ function ensureElementHelpers(): void { }; } +function nodeConstructor(): typeof Node { + const defaultView = document.defaultView; + if (defaultView === null) throw new Error("Expected document.defaultView to install Obsidian DOM helpers."); + return (globalThis as typeof globalThis & { Node?: typeof Node }).Node ?? defaultView.Node; +} + function applyOptions(element: HTMLElement, options: ElementOptions): void { if (Array.isArray(options.cls)) { element.classList.add(...options.cls.filter(Boolean)); diff --git a/tests/user-input/user-input.test.ts b/tests/user-input/user-input.test.ts index 82d19838..30623472 100644 --- a/tests/user-input/user-input.test.ts +++ b/tests/user-input/user-input.test.ts @@ -40,25 +40,27 @@ describe("user input model", () => { }); it("signs pending request content and drafts deterministically", () => { - const input = expectPresent(toPendingUserInput({ - id: 7, - method: "item/tool/requestUserInput", - params: { - threadId: "thread", - turnId: "turn", - itemId: "item", - questions: [ - { - id: "direction", - header: "Direction", - question: "Which way?", - isOther: true, - isSecret: false, - options: [{ label: "Recommended", description: "Use the default path" }], - }, - ], - }, - })); + const input = expectPresent( + toPendingUserInput({ + id: 7, + method: "item/tool/requestUserInput", + params: { + threadId: "thread", + turnId: "turn", + itemId: "item", + questions: [ + { + id: "direction", + header: "Direction", + question: "Which way?", + isOther: true, + isSecret: false, + options: [{ label: "Recommended", description: "Use the default path" }], + }, + ], + }, + }), + ); const drafts = new Map([ ["z", "last"], ["a", "first"],