From b9875a488f374ffdc886eb5602001bb72c85a97b Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:21:09 +0900 Subject: [PATCH 1/7] feat: add backend support for file change diffs - Add getSessionDiff() method to OpenCodeClient - Add getSessionDiff/openDiffEditor message handlers in ChatViewProvider - Register opencode-diff-before/after TextDocumentContentProviders - Export FileDiff type from opencode-client --- src/chat-view-provider.ts | 23 ++++++++++++++++++++++- src/extension.ts | 12 ++++++++++++ src/opencode-client.ts | 13 ++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/chat-view-provider.ts b/src/chat-view-provider.ts index 6f9d041..fcf7018 100644 --- a/src/chat-view-provider.ts +++ b/src/chat-view-provider.ts @@ -3,6 +3,7 @@ import * as path from "node:path"; import * as vscode from "vscode"; import type { Event, + FileDiff, Message, OpenCodeConnection, OpenCodePath, @@ -36,7 +37,8 @@ export type ExtToWebviewMessage = | { type: "contextUsage"; usage: { inputTokens: number; contextLimit: number } } | { type: "toolConfig"; paths: OpenCodePath } | { type: "locale"; vscodeLanguage: string } - | { type: "modelUpdated"; model: string; default: Record }; + | { type: "modelUpdated"; model: string; default: Record } + | { type: "sessionDiff"; sessionId: string; diffs: FileDiff[] }; // --- Webview → Extension Host --- export type WebviewToExtMessage = @@ -71,6 +73,8 @@ export type WebviewToExtMessage = | { type: "openConfigFile"; filePath: string } | { type: "openTerminal" } | { type: "setModel"; model: string } + | { type: "getSessionDiff"; sessionId: string } + | { type: "openDiffEditor"; filePath: string; before: string; after: string } | { type: "ready" }; export class ChatViewProvider implements vscode.WebviewViewProvider { @@ -326,6 +330,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.postMessage({ type: "modelUpdated", model: message.model, default: {} }); break; } + case "getSessionDiff": { + const diffs = await this.connection.getSessionDiff(message.sessionId); + this.postMessage({ type: "sessionDiff", sessionId: message.sessionId, diffs }); + break; + } + case "openDiffEditor": { + // 仮想ドキュメントを使って VS Code のネイティブ diff エディタを開く + const beforeUri = vscode.Uri.parse( + `opencode-diff-before:${message.filePath}?${encodeURIComponent(message.before)}`, + ); + const afterUri = vscode.Uri.parse( + `opencode-diff-after:${message.filePath}?${encodeURIComponent(message.after)}`, + ); + const fileName = path.basename(message.filePath); + await vscode.commands.executeCommand("vscode.diff", beforeUri, afterUri, `${fileName} (Changes)`); + break; + } } } diff --git a/src/extension.ts b/src/extension.ts index b103be6..99fcb72 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,6 +41,18 @@ export async function activate(context: vscode.ExtensionContext) { const chatViewProvider = new ChatViewProvider(context.extensionUri, connection); context.subscriptions.push(vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chatViewProvider)); + // diff エディタ用の仮想ドキュメントプロバイダー。 + // URI のクエリ部分にエンコードされたコンテンツを返す。 + const diffContentProvider: vscode.TextDocumentContentProvider = { + provideTextDocumentContent(uri: vscode.Uri): string { + return decodeURIComponent(uri.query); + }, + }; + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider("opencode-diff-before", diffContentProvider), + vscode.workspace.registerTextDocumentContentProvider("opencode-diff-after", diffContentProvider), + ); + context.subscriptions.push(new vscode.Disposable(() => connection.disconnect())); } diff --git a/src/opencode-client.ts b/src/opencode-client.ts index 2697e37..09fea66 100644 --- a/src/opencode-client.ts +++ b/src/opencode-client.ts @@ -4,6 +4,7 @@ import { createOpencodeClient, createOpencodeServer, type Event, + type FileDiff, type McpStatus, type Message, type Path as OpenCodePath, @@ -15,7 +16,7 @@ import { } from "@opencode-ai/sdk"; import * as vscode from "vscode"; -export type { Event, Session, Message, Part, Provider, McpStatus, ToolListItem, Config, OpenCodePath }; +export type { Event, Session, Message, Part, Provider, McpStatus, ToolListItem, Config, OpenCodePath, FileDiff }; // provider.list() が返す生データ型 export type ProviderListResult = { @@ -245,6 +246,16 @@ export class OpenCodeConnection { }); } + // --- Session Diff API --- + + async getSessionDiff(sessionId: string): Promise { + const client = this.requireClient(); + const response = await client.session.diff({ + path: { id: sessionId }, + }); + return response.data!; + } + // --- Revert API --- async revertSession(sessionId: string, messageID: string): Promise { From 56bc6a673aec8cbf113d54147d8facc400418410 Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:21:21 +0900 Subject: [PATCH 2/7] feat: add message types, locale strings, and icons for file diffs - Add sessionDiff, getSessionDiff, openDiffEditor message types - Add fileChanges.* locale strings (en/ja) - Add DiffIcon and ExternalLinkIcon components --- webview/components/atoms/icons/icons.tsx | 18 ++++++++++++++++++ webview/locales/en.ts | 6 ++++++ webview/locales/ja.ts | 6 ++++++ webview/vscode-api.ts | 7 +++++-- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/webview/components/atoms/icons/icons.tsx b/webview/components/atoms/icons/icons.tsx index 324c773..c842e39 100644 --- a/webview/components/atoms/icons/icons.tsx +++ b/webview/components/atoms/icons/icons.tsx @@ -245,6 +245,24 @@ export function ToolIcon({ width = 14, height = 14, ...props }: IconProps) { // ─── Visibility ─────────────────────────────────────────────────────── +/** Codicon: diff — git compare icon for file changes */ +export function DiffIcon({ width = 16, height = 16, ...props }: IconProps) { + return ( + + ); +} + +/** External link — open file in editor */ +export function ExternalLinkIcon({ width = 12, height = 12, ...props }: IconProps) { + return ( + + ); +} + /** Eye icon — visible / show */ export function EyeIcon({ width = 14, height = 14, ...props }: IconProps) { return ( diff --git a/webview/locales/en.ts b/webview/locales/en.ts index 0b8339f..54bcb7e 100644 --- a/webview/locales/en.ts +++ b/webview/locales/en.ts @@ -100,6 +100,12 @@ export const en = { "config.langAuto": "Auto (VS Code)", "config.langEn": "English", "config.langJa": "日本語", + + // FileChangesHeader + "fileChanges.title": "File Changes", + "fileChanges.noChanges": "No file changes", + "fileChanges.openDiff": "Open in diff editor", + "fileChanges.toggle": "File changes", } as const; export type LocaleKeys = keyof typeof en; diff --git a/webview/locales/ja.ts b/webview/locales/ja.ts index 5b10a29..274da8e 100644 --- a/webview/locales/ja.ts +++ b/webview/locales/ja.ts @@ -102,4 +102,10 @@ export const ja: typeof en = { "config.langAuto": "自動 (VS Code)", "config.langEn": "English", "config.langJa": "日本語", + + // FileChangesHeader + "fileChanges.title": "ファイル変更", + "fileChanges.noChanges": "ファイル変更なし", + "fileChanges.openDiff": "差分エディタで開く", + "fileChanges.toggle": "ファイル変更", }; diff --git a/webview/vscode-api.ts b/webview/vscode-api.ts index 05b2bf2..fd32caf 100644 --- a/webview/vscode-api.ts +++ b/webview/vscode-api.ts @@ -3,7 +3,7 @@ * Extension Host 側の chat-view-provider.ts で定義したプロトコルに対応する。 */ -import type { Event, Message, Part, Provider, Session } from "@opencode-ai/sdk"; +import type { Event, FileDiff, Message, Part, Provider, Session } from "@opencode-ai/sdk"; // --- File attachment --- export type FileAttachment = { @@ -65,7 +65,8 @@ export type ExtToWebviewMessage = | { type: "contextUsage"; usage: { inputTokens: number; contextLimit: number } } | { type: "toolConfig"; paths: { home: string; config: string; state: string; directory: string } } | { type: "locale"; vscodeLanguage: string } - | { type: "modelUpdated"; model: string; default: Record }; + | { type: "modelUpdated"; model: string; default: Record } + | { type: "sessionDiff"; sessionId: string; diffs: FileDiff[] }; // --- Webview → Extension Host --- export type WebviewToExtMessage = @@ -100,6 +101,8 @@ export type WebviewToExtMessage = | { type: "openConfigFile"; filePath: string } | { type: "openTerminal" } | { type: "setModel"; model: string } + | { type: "getSessionDiff"; sessionId: string } + | { type: "openDiffEditor"; filePath: string; before: string; after: string } | { type: "ready" }; interface VsCodeApi { From faa8163f2a665b4209607fe97e94acb1bbee1819 Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:21:25 +0900 Subject: [PATCH 3/7] feat: add useFileChanges hook for managing file diff state - Handle session.diff SSE events - Provide diffs/setDiffs/clearDiffs/handleFileChangeEvent --- webview/hooks/useFileChanges.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 webview/hooks/useFileChanges.ts diff --git a/webview/hooks/useFileChanges.ts b/webview/hooks/useFileChanges.ts new file mode 100644 index 0000000..b164417 --- /dev/null +++ b/webview/hooks/useFileChanges.ts @@ -0,0 +1,30 @@ +import type { Event, FileDiff } from "@opencode-ai/sdk"; +import { useCallback, useState } from "react"; + +/** + * セッションレベルのファイル変更差分を管理するフック。 + * + * SSE の `file.edited` / `session.diff` イベントに応じて差分データを更新し、 + * FileChangesHeader に提供する。 + */ +export function useFileChanges() { + const [diffs, setDiffs] = useState([]); + + const clearDiffs = useCallback(() => { + setDiffs([]); + }, []); + + // SSE event handler for file-change-related events + const handleFileChangeEvent = useCallback((event: Event) => { + if (event.type === "session.diff") { + setDiffs(event.properties.diff); + } + }, []); + + return { + diffs, + setDiffs, + clearDiffs, + handleFileChangeEvent, + } as const; +} From 456aaa80e7983525d77d2048e8e23050945a943f Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:21:30 +0900 Subject: [PATCH 4/7] feat: add FileChangesHeader collapsible component - TodoHeader-style collapsible bar with file count and stats - Per-file items with status badge (A/D/M), inline diff, open button - Expandable inline DiffView for each file --- .../FileChangesHeader.module.css | 168 ++++++++++++++++++ .../FileChangesHeader/FileChangesHeader.tsx | 121 +++++++++++++ .../molecules/FileChangesHeader/index.ts | 1 + 3 files changed, 290 insertions(+) create mode 100644 webview/components/molecules/FileChangesHeader/FileChangesHeader.module.css create mode 100644 webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx create mode 100644 webview/components/molecules/FileChangesHeader/index.ts diff --git a/webview/components/molecules/FileChangesHeader/FileChangesHeader.module.css b/webview/components/molecules/FileChangesHeader/FileChangesHeader.module.css new file mode 100644 index 0000000..8aaa84e --- /dev/null +++ b/webview/components/molecules/FileChangesHeader/FileChangesHeader.module.css @@ -0,0 +1,168 @@ +.root { + border-top: 1px solid var(--vscode-panel-border); + background-color: var(--vscode-sideBar-background); +} + +.bar { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + cursor: pointer; + user-select: none; + font-size: 12px; +} + +.bar:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.icon { + flex-shrink: 0; + color: var(--vscode-textLink-foreground); +} + +.label { + font-weight: 600; + font-size: 11px; + color: var(--vscode-foreground); +} + +.count { + font-size: 11px; + color: var(--vscode-descriptionForeground); + font-family: var(--vscode-editor-font-family, monospace); +} + +.stats { + display: flex; + gap: 4px; + flex: 1; + font-size: 11px; + font-family: var(--vscode-editor-font-family, monospace); +} + +.chevron { + display: flex; + align-items: center; + flex-shrink: 0; + color: var(--vscode-descriptionForeground); + transition: transform 0.15s; +} + +.chevron.expanded { + transform: rotate(90deg); +} + +/* --- File List --- */ + +.list { + padding: 0 8px 6px 12px; +} + +/* --- File Item --- */ + +.fileItem { + border-bottom: 1px solid color-mix(in srgb, var(--vscode-panel-border) 50%, transparent); +} + +.fileItem:last-child { + border-bottom: none; +} + +.fileHeader { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 6px; + cursor: pointer; + font-size: 12px; + border-radius: 4px; +} + +.fileHeader:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.fileChevron { + display: flex; + align-items: center; + flex-shrink: 0; + color: var(--vscode-descriptionForeground); + transition: transform 0.15s; +} + +.fileChevronExpanded { + transform: rotate(90deg); +} + +.statusBadge { + font-size: 10px; + font-weight: 700; + width: 16px; + text-align: center; + flex-shrink: 0; + border-radius: 2px; + line-height: 16px; +} + +.statusAdded { + color: var(--vscode-gitDecoration-addedResourceForeground, #2ea043); +} + +.statusDeleted { + color: var(--vscode-gitDecoration-deletedResourceForeground, #f85149); +} + +.statusModified { + color: var(--vscode-editorWarning-foreground, #ff9800); +} + +.fileName { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 1; + min-width: 0; +} + +.filePath { + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; +} + +.fileStats { + display: flex; + gap: 4px; + flex-shrink: 0; + font-size: 11px; + font-family: var(--vscode-editor-font-family, monospace); +} + +.statAdd { + color: var(--vscode-gitDecoration-addedResourceForeground, #2ea043); +} + +.statRemove { + color: var(--vscode-gitDecoration-deletedResourceForeground, #f85149); +} + +.openButton { + opacity: 0; + transition: opacity 0.15s; + flex-shrink: 0; +} + +.fileHeader:hover .openButton { + opacity: 1; +} + +.diffBody { + padding: 0 6px 6px 28px; +} diff --git a/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx b/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx new file mode 100644 index 0000000..079ee47 --- /dev/null +++ b/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx @@ -0,0 +1,121 @@ +import type { FileDiff } from "@opencode-ai/sdk"; +import { useState } from "react"; +import { useLocale } from "../../../locales"; +import { IconButton } from "../../atoms/IconButton"; +import { ChevronRightIcon, DiffIcon, ExternalLinkIcon } from "../../atoms/icons"; +import { DiffView } from "../DiffView"; +import styles from "./FileChangesHeader.module.css"; + +type Props = { + diffs: FileDiff[]; + onOpenDiffEditor: (filePath: string, before: string, after: string) => void; +}; + +/** ファイルパスから basename を取得 */ +function basename(filePath: string): string { + const parts = filePath.split("/"); + return parts[parts.length - 1] || filePath; +} + +/** ファイルパスからディレクトリ部分を取得 */ +function dirname(filePath: string): string { + const lastSlash = filePath.lastIndexOf("/"); + if (lastSlash < 0) return ""; + return filePath.slice(0, lastSlash); +} + +/** ファイルの変更種別を判定 */ +function getFileStatus(diff: FileDiff): "added" | "deleted" | "modified" { + if (diff.before === "" && diff.after !== "") return "added"; + if (diff.before !== "" && diff.after === "") return "deleted"; + return "modified"; +} + +function FileChangeItem({ + diff, + onOpenDiffEditor, +}: { diff: FileDiff; onOpenDiffEditor: (filePath: string, before: string, after: string) => void }) { + const t = useLocale(); + const [expanded, setExpanded] = useState(false); + const status = getFileStatus(diff); + const fileName = basename(diff.file); + const dirName = dirname(diff.file); + + const statusClass: Record = { + added: styles.statusAdded, + deleted: styles.statusDeleted, + modified: styles.statusModified, + }; + + const statusLabel: Record = { + added: "A", + deleted: "D", + modified: "M", + }; + + return ( +
+
setExpanded((s) => !s)}> + + + + {statusLabel[status]} + + {fileName} + + {dirName && {dirName}} + + {diff.additions > 0 && +{diff.additions}} + {diff.deletions > 0 && −{diff.deletions}} + + { + e.stopPropagation(); + onOpenDiffEditor(diff.file, diff.before, diff.after); + }} + title={t["fileChanges.openDiff"]} + > + + +
+ {expanded && ( +
+ +
+ )} +
+ ); +} + +export function FileChangesHeader({ diffs, onOpenDiffEditor }: Props) { + const t = useLocale(); + const [expanded, setExpanded] = useState(false); + + const totalAdditions = diffs.reduce((sum, d) => sum + d.additions, 0); + const totalDeletions = diffs.reduce((sum, d) => sum + d.deletions, 0); + + return ( +
+
setExpanded((s) => !s)} title={t["fileChanges.toggle"]}> + + {t["fileChanges.title"]} + {diffs.length} + + {totalAdditions > 0 && +{totalAdditions}} + {totalDeletions > 0 && −{totalDeletions}} + + + + +
+ {expanded && ( +
+ {diffs.map((diff) => ( + + ))} +
+ )} +
+ ); +} diff --git a/webview/components/molecules/FileChangesHeader/index.ts b/webview/components/molecules/FileChangesHeader/index.ts new file mode 100644 index 0000000..7ab4fc0 --- /dev/null +++ b/webview/components/molecules/FileChangesHeader/index.ts @@ -0,0 +1 @@ +export { FileChangesHeader } from "./FileChangesHeader"; From 7836f4f3b063b3067ad1c855c7cad030da3f36ee Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:21:36 +0900 Subject: [PATCH 5/7] feat: integrate file changes into App - Wire useFileChanges hook and sessionDiff message handling in App - Add fileDiffs/onOpenDiffEditor to AppContext - Render FileChangesHeader below TodoHeader above InputArea --- webview/App.tsx | 36 ++++++++++++++++++- .../organisms/ChatHeader/ChatHeader.tsx | 6 +++- webview/contexts/AppContext.tsx | 6 +++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/webview/App.tsx b/webview/App.tsx index 875cf43..38a3fab 100644 --- a/webview/App.tsx +++ b/webview/App.tsx @@ -1,12 +1,14 @@ import type { Event } from "@opencode-ai/sdk"; import { useCallback, useEffect, useState } from "react"; import { EmptyState } from "./components/molecules/EmptyState"; +import { FileChangesHeader } from "./components/molecules/FileChangesHeader"; import { TodoHeader } from "./components/molecules/TodoHeader"; import { ChatHeader } from "./components/organisms/ChatHeader"; import { InputArea } from "./components/organisms/InputArea"; import { MessagesArea } from "./components/organisms/MessagesArea"; import { SessionList } from "./components/organisms/SessionList"; import { AppContextProvider, type AppContextValue } from "./contexts/AppContext"; +import { useFileChanges } from "./hooks/useFileChanges"; import { useLocale } from "./hooks/useLocale"; import { useMessages } from "./hooks/useMessages"; import { usePermissions } from "./hooks/usePermissions"; @@ -25,6 +27,7 @@ export function App() { const prov = useProviders(); const perm = usePermissions(); const locale = useLocale(); + const fileChanges = useFileChanges(); // Extension Host → Webview メッセージでのみ更新される単純なステート const [openEditors, setOpenEditors] = useState([]); @@ -50,8 +53,20 @@ export function App() { session.handleSessionEvent(event); msg.handleMessageEvent(event); perm.handlePermissionEvent(event); + fileChanges.handleFileChangeEvent(event); + + // file.edited イベント時にセッション差分を再取得する + if (event.type === "file.edited" && session.activeSession) { + postMessage({ type: "getSessionDiff", sessionId: session.activeSession.id }); + } }, - [session.handleSessionEvent, msg.handleMessageEvent, perm.handlePermissionEvent], + [ + session.handleSessionEvent, + session.activeSession, + msg.handleMessageEvent, + perm.handlePermissionEvent, + fileChanges.handleFileChangeEvent, + ], ); // Extension Host → Webview message listener @@ -71,8 +86,10 @@ export function App() { session.setActiveSession(data.session); if (data.session) { postMessage({ type: "getMessages", sessionId: data.session.id }); + postMessage({ type: "getSessionDiff", sessionId: data.session.id }); } else { msg.setMessages([]); + fileChanges.clearDiffs(); } break; case "event": @@ -117,6 +134,12 @@ export function App() { } break; } + case "sessionDiff": { + if (data.sessionId === session.activeSession?.id) { + fileChanges.setDiffs(data.diffs); + } + break; + } } }; window.addEventListener("message", handler); @@ -128,6 +151,8 @@ export function App() { handleEvent, locale.setVscodeLanguage, msg.setMessages, + fileChanges.setDiffs, + fileChanges.clearDiffs, prov.setAllProvidersData, prov.setProviders, prov.setSelectedModel, @@ -229,6 +254,10 @@ export function App() { [session.activeSession, msg], ); + const handleOpenDiffEditor = useCallback((filePath: string, before: string, after: string) => { + postMessage({ type: "openDiffEditor", filePath, before, after }); + }, []); + const contextValue: AppContextValue = { sessions: session.sessions, activeSession: session.activeSession, @@ -251,6 +280,8 @@ export function App() { permissions: perm.permissions, openEditors, workspaceFiles, + fileDiffs: fileChanges.diffs, + onOpenDiffEditor: handleOpenDiffEditor, onSend: handleSend, onShellExecute: handleShellExecute, isShellMessage: msg.isShellMessage, @@ -295,6 +326,9 @@ export function App() { onRevertToCheckpoint={handleRevertToCheckpoint} /> {msg.latestTodos.length > 0 && } + {fileChanges.diffs.length > 0 && ( + + )} void; }; -export function ChatHeader({ activeSession, onNewSession, onToggleSessionList }: Props) { +export function ChatHeader({ + activeSession, + onNewSession, + onToggleSessionList, +}: Props) { const t = useLocale(); return (
diff --git a/webview/contexts/AppContext.tsx b/webview/contexts/AppContext.tsx index 6dc7c10..95611cd 100644 --- a/webview/contexts/AppContext.tsx +++ b/webview/contexts/AppContext.tsx @@ -1,4 +1,4 @@ -import type { Permission, Provider, Session } from "@opencode-ai/sdk"; +import type { FileDiff, Permission, Provider, Session } from "@opencode-ai/sdk"; import { createContext, useContext } from "react"; import type { MessageWithParts } from "../hooks/useMessages"; import type { LocaleSetting } from "../locales"; @@ -37,6 +37,10 @@ export type AppContextValue = { openEditors: FileAttachment[]; workspaceFiles: FileAttachment[]; + // File Changes + fileDiffs: FileDiff[]; + onOpenDiffEditor: (filePath: string, before: string, after: string) => void; + // Actions onSend: (text: string, files: FileAttachment[]) => void; onShellExecute: (command: string) => void; From 06f82f8b185064a41293e97c4096304808b92313 Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:21:41 +0900 Subject: [PATCH 6/7] test: add tests for file change diff feature - Add DiffIcon/ExternalLinkIcon to icons unit tests (+16 tests) - Add FileChangesHeader unit tests (22 tests) - Add useFileChanges hook tests (5 tests) - Add scenario test 15-file-changes (12 tests) --- .../__tests__/components/atoms/icons.test.tsx | 10 + .../molecules/FileChangesHeader.test.tsx | 262 ++++++++++++++++++ .../__tests__/hooks/useFileChanges.test.ts | 77 +++++ .../scenarios/15-file-changes.test.tsx | 209 ++++++++++++++ 4 files changed, 558 insertions(+) create mode 100644 webview/__tests__/components/molecules/FileChangesHeader.test.tsx create mode 100644 webview/__tests__/hooks/useFileChanges.test.ts create mode 100644 webview/__tests__/scenarios/15-file-changes.test.tsx diff --git a/webview/__tests__/components/atoms/icons.test.tsx b/webview/__tests__/components/atoms/icons.test.tsx index fbfa45e..3c30d9a 100644 --- a/webview/__tests__/components/atoms/icons.test.tsx +++ b/webview/__tests__/components/atoms/icons.test.tsx @@ -9,9 +9,11 @@ import { ClipIcon, CloseIcon, DeleteIcon, + DiffIcon, EditActionIcon, EditIcon, ErrorCircleIcon, + ExternalLinkIcon, EyeIcon, EyeOffIcon, FileIcon, @@ -210,3 +212,11 @@ describe("EyeIcon", () => { describe("EyeOffIcon", () => { describeIconComponent(EyeOffIcon, 14, 14); }); + +describe("DiffIcon", () => { + describeIconComponent(DiffIcon, 16, 16); +}); + +describe("ExternalLinkIcon", () => { + describeIconComponent(ExternalLinkIcon, 12, 12); +}); diff --git a/webview/__tests__/components/molecules/FileChangesHeader.test.tsx b/webview/__tests__/components/molecules/FileChangesHeader.test.tsx new file mode 100644 index 0000000..1f7c079 --- /dev/null +++ b/webview/__tests__/components/molecules/FileChangesHeader.test.tsx @@ -0,0 +1,262 @@ +import type { FileDiff } from "@opencode-ai/sdk"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { FileChangesHeader } from "../../../components/molecules/FileChangesHeader"; + +function createFileDiff(overrides: Partial = {}): FileDiff { + return { + file: "src/index.ts", + before: "const a = 1;", + after: "const a = 2;", + additions: 1, + deletions: 1, + ...overrides, + }; +} + +describe("FileChangesHeader", () => { + const defaultProps = { + diffs: [createFileDiff()], + onOpenDiffEditor: vi.fn(), + }; + + // default rendering + context("デフォルトの描画の場合", () => { + // renders the header bar + it("ヘッダーバーをレンダリングすること", () => { + const { container } = render(); + expect(container.querySelector(".bar")).toBeInTheDocument(); + }); + + // shows file count + it("ファイル数を表示すること", () => { + const { container } = render(); + expect(container.querySelector(".count")?.textContent).toBe("1"); + }); + + // shows additions and deletions stats + it("追加・削除の統計を表示すること", () => { + const { container } = render(); + const addStat = container.querySelector(".statAdd"); + const removeStat = container.querySelector(".statRemove"); + expect(addStat?.textContent).toBe("+1"); + expect(removeStat?.textContent).toBe("−1"); + }); + + // list is collapsed by default + it("リストはデフォルトで折りたたまれていること", () => { + const { container } = render(); + expect(container.querySelector(".list")).not.toBeInTheDocument(); + }); + }); + + // when header is clicked + context("ヘッダーをクリックした場合", () => { + // expands the file list + it("ファイルリストを展開すること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + expect(container.querySelector(".list")).toBeInTheDocument(); + }); + + // shows file name + it("ファイル名を表示すること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const fileName = container.querySelector(".fileName"); + expect(fileName?.textContent).toBe("index.ts"); + }); + + // shows status badge M for modified file + it("変更ファイルにステータスバッジ M を表示すること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const badge = container.querySelector(".statusBadge"); + expect(badge?.textContent).toBe("M"); + }); + }); + + // added file + context("新規追加ファイルの場合", () => { + // renders status badge A + it("ステータスバッジ A を表示すること", () => { + const diffs = [createFileDiff({ before: "", after: "new content", file: "new.ts" })]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const badge = container.querySelector(".statusBadge"); + expect(badge?.textContent).toBe("A"); + }); + }); + + // deleted file + context("削除ファイルの場合", () => { + // renders status badge D + it("ステータスバッジ D を表示すること", () => { + const diffs = [createFileDiff({ before: "old content", after: "", file: "deleted.ts" })]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const badge = container.querySelector(".statusBadge"); + expect(badge?.textContent).toBe("D"); + }); + }); + + // expand inline diff + context("ファイルアイテムをクリックした場合", () => { + // shows inline diff view + it("インライン差分ビューを表示すること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const fileHeader = container.querySelector(".fileHeader"); + fireEvent.click(fileHeader!); + expect(screen.getByTestId("diff-view")).toBeTruthy(); + }); + }); + + // multiple files + context("複数ファイルがある場合", () => { + // renders all files + it("全ファイルを表示すること", () => { + const diffs = [ + createFileDiff({ file: "src/a.ts" }), + createFileDiff({ file: "src/b.ts" }), + createFileDiff({ file: "src/c.ts" }), + ]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const fileItems = container.querySelectorAll(".fileItem"); + expect(fileItems.length).toBe(3); + }); + + // shows correct total stats in header + it("ヘッダーに合計の追加・削除数を表示すること", () => { + const diffs = [ + createFileDiff({ file: "a.ts", additions: 3, deletions: 1 }), + createFileDiff({ file: "b.ts", additions: 5, deletions: 2 }), + ]; + const { container } = render(); + const stats = container.querySelector(".stats"); + expect(stats?.textContent).toContain("+8"); + expect(stats?.textContent).toContain("−3"); + }); + }); + + // directory path display + context("ディレクトリパスがある場合", () => { + // shows directory path + it("ディレクトリパスを表示すること", () => { + const diffs = [createFileDiff({ file: "src/components/Button.tsx" })]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const filePath = container.querySelector(".filePath"); + expect(filePath?.textContent).toBe("src/components"); + }); + }); + + // open diff editor + context("外部リンクボタンをクリックした場合", () => { + // calls onOpenDiffEditor + it("onOpenDiffEditor が呼ばれること", () => { + const onOpenDiffEditor = vi.fn(); + const diff = createFileDiff({ file: "src/app.ts", before: "old", after: "new" }); + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const openButton = container.querySelector(".openButton"); + fireEvent.click(openButton!); + expect(onOpenDiffEditor).toHaveBeenCalledWith("src/app.ts", "old", "new"); + }); + + // does not toggle file expansion + it("ファイルの展開がトグルされないこと", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const openButton = container.querySelector(".openButton"); + fireEvent.click(openButton!); + expect(container.querySelector(".diffBody")).not.toBeInTheDocument(); + }); + }); + + // toggle collapse + context("ヘッダーを2回クリックした場合", () => { + // collapses the file list + it("ファイルリストが折りたたまれること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + expect(container.querySelector(".list")).toBeInTheDocument(); + fireEvent.click(container.querySelector(".bar")!); + expect(container.querySelector(".list")).not.toBeInTheDocument(); + }); + }); + + // chevron rotation + context("展開時のシェブロンの場合", () => { + // applies expanded class to chevron + it("chevron に expanded クラスが付与されること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + expect(container.querySelector(".chevron")).toHaveClass("expanded"); + }); + }); + + // no directory path for root-level file + context("ルートレベルのファイルの場合", () => { + // does not show directory path + it("ディレクトリパスが表示されないこと", () => { + const diffs = [createFileDiff({ file: "README.md" })]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + expect(container.querySelector(".filePath")).not.toBeInTheDocument(); + }); + }); + + // zero additions/deletions + context("追加・削除が 0 の場合", () => { + // does not show addition stat + it("追加統計が表示されないこと", () => { + const diffs = [createFileDiff({ additions: 0, deletions: 3 })]; + const { container } = render(); + expect(container.querySelector(".statAdd")).not.toBeInTheDocument(); + }); + + // does not show deletion stat + it("削除統計が表示されないこと", () => { + const diffs = [createFileDiff({ additions: 5, deletions: 0 })]; + const { container } = render(); + expect(container.querySelector(".statRemove")).not.toBeInTheDocument(); + }); + }); + + // file item inline diff toggle + context("ファイルアイテムを2回クリックした場合", () => { + // collapses the inline diff + it("インライン差分が折りたたまれること", () => { + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const fileHeader = container.querySelector(".fileHeader")!; + fireEvent.click(fileHeader); + expect(container.querySelector(".diffBody")).toBeInTheDocument(); + fireEvent.click(fileHeader); + expect(container.querySelector(".diffBody")).not.toBeInTheDocument(); + }); + }); + + // file item per-file stats + context("ファイルアイテムの個別統計の場合", () => { + // shows per-file additions + it("ファイルごとの追加数を表示すること", () => { + const diffs = [createFileDiff({ file: "a.ts", additions: 7, deletions: 2 })]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const fileStats = container.querySelector(".fileStats"); + expect(fileStats?.querySelector(".statAdd")?.textContent).toBe("+7"); + }); + + // shows per-file deletions + it("ファイルごとの削除数を表示すること", () => { + const diffs = [createFileDiff({ file: "a.ts", additions: 7, deletions: 2 })]; + const { container } = render(); + fireEvent.click(container.querySelector(".bar")!); + const fileStats = container.querySelector(".fileStats"); + expect(fileStats?.querySelector(".statRemove")?.textContent).toBe("−2"); + }); + }); +}); diff --git a/webview/__tests__/hooks/useFileChanges.test.ts b/webview/__tests__/hooks/useFileChanges.test.ts new file mode 100644 index 0000000..73a84c1 --- /dev/null +++ b/webview/__tests__/hooks/useFileChanges.test.ts @@ -0,0 +1,77 @@ +import type { Event, FileDiff } from "@opencode-ai/sdk"; +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { useFileChanges } from "../../hooks/useFileChanges"; + +function createFileDiff(overrides: Partial = {}): FileDiff { + return { + file: "src/index.ts", + before: "const a = 1;", + after: "const a = 2;", + additions: 1, + deletions: 1, + ...overrides, + }; +} + +describe("useFileChanges", () => { + // initial state + context("初期状態の場合", () => { + // diffs is empty + it("diffs が空配列であること", () => { + const { result } = renderHook(() => useFileChanges()); + expect(result.current.diffs).toEqual([]); + }); + }); + + // setDiffs + context("setDiffs を呼んだ場合", () => { + // updates diffs state + it("diffs が更新されること", () => { + const { result } = renderHook(() => useFileChanges()); + const diff = createFileDiff(); + act(() => result.current.setDiffs([diff])); + expect(result.current.diffs).toEqual([diff]); + }); + }); + + // clearDiffs + context("clearDiffs を呼んだ場合", () => { + // clears diffs + it("diffs が空になること", () => { + const { result } = renderHook(() => useFileChanges()); + act(() => result.current.setDiffs([createFileDiff()])); + act(() => result.current.clearDiffs()); + expect(result.current.diffs).toEqual([]); + }); + }); + + // handleFileChangeEvent + context("session.diff イベントを受け取った場合", () => { + // updates diffs from event + it("イベントの diff データで diffs を更新すること", () => { + const { result } = renderHook(() => useFileChanges()); + const diff = createFileDiff({ file: "updated.ts", additions: 10, deletions: 3 }); + const event = { + type: "session.diff", + properties: { sessionID: "s1", diff: [diff] }, + } as unknown as Event; + act(() => result.current.handleFileChangeEvent(event)); + expect(result.current.diffs).toEqual([diff]); + }); + }); + + // ignores unrelated events + context("関係ないイベントを受け取った場合", () => { + // does not change diffs + it("diffs が変更されないこと", () => { + const { result } = renderHook(() => useFileChanges()); + const event = { + type: "message.updated", + properties: {}, + } as unknown as Event; + act(() => result.current.handleFileChangeEvent(event)); + expect(result.current.diffs).toEqual([]); + }); + }); +}); diff --git a/webview/__tests__/scenarios/15-file-changes.test.tsx b/webview/__tests__/scenarios/15-file-changes.test.tsx new file mode 100644 index 0000000..9ce077d --- /dev/null +++ b/webview/__tests__/scenarios/15-file-changes.test.tsx @@ -0,0 +1,209 @@ +import { screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { postMessage } from "../../vscode-api"; +import { createSession } from "../factories"; +import { renderApp, sendExtMessage } from "../helpers"; + +/** sessionDiff メッセージでファイル変更差分を送信する */ +async function sendSessionDiff( + sessionId: string, + diffs: Array<{ file: string; before: string; after: string; additions: number; deletions: number }>, +) { + await sendExtMessage({ + type: "sessionDiff", + sessionId, + diffs, + }); +} + +/** アクティブセッションとファイル変更差分をセットアップする */ +async function setupWithDiffs( + diffs: Array<{ file: string; before: string; after: string; additions: number; deletions: number }>, +) { + renderApp(); + await sendExtMessage({ type: "activeSession", session: createSession({ id: "s1" }) }); + await sendSessionDiff("s1", diffs); +} + +// File changes diff display +describe("ファイル変更差分表示", () => { + // FileChangesHeader is shown when sessionDiff arrives + context("sessionDiff メッセージを受信した場合", () => { + beforeEach(async () => { + await setupWithDiffs([ + { file: "src/index.ts", before: "const a = 1;", after: "const a = 2;", additions: 1, deletions: 1 }, + { file: "src/utils.ts", before: "", after: "export const b = 2;", additions: 1, deletions: 0 }, + ]); + }); + + // Shows File Changes label + it("File Changes ラベルが表示されること", () => { + expect(screen.getByText("File Changes")).toBeInTheDocument(); + }); + + // Shows file count + it("ファイル数が表示されること", () => { + expect(screen.getByText("2")).toBeInTheDocument(); + }); + }); + + // FileChangesHeader is hidden when there are no diffs + context("ファイル変更がない場合", () => { + // does not show FileChangesHeader + it("FileChangesHeader が表示されないこと", async () => { + renderApp(); + await sendExtMessage({ type: "activeSession", session: createSession({ id: "s1" }) }); + expect(screen.queryByText("File Changes")).not.toBeInTheDocument(); + }); + }); + + // Expanding shows file details + context("FileChangesHeader を展開した場合", () => { + beforeEach(async () => { + await setupWithDiffs([ + { file: "src/app.ts", before: "old code", after: "new code", additions: 1, deletions: 1 }, + ]); + const user = userEvent.setup(); + await user.click(screen.getByTitle("File changes")); + }); + + // Shows file name + it("ファイル名が表示されること", () => { + expect(screen.getByText("app.ts")).toBeInTheDocument(); + }); + + // Shows directory + it("ディレクトリパスが表示されること", () => { + expect(screen.getByText("src")).toBeInTheDocument(); + }); + }); + + // sessionDiff for a different session is ignored + context("異なるセッションの sessionDiff を受信した場合", () => { + // does not show FileChangesHeader + it("FileChangesHeader が表示されないこと", async () => { + renderApp(); + await sendExtMessage({ type: "activeSession", session: createSession({ id: "s1" }) }); + await sendSessionDiff("other-session", [ + { file: "src/x.ts", before: "", after: "code", additions: 1, deletions: 0 }, + ]); + expect(screen.queryByText("File Changes")).not.toBeInTheDocument(); + }); + }); + + // Diffs are cleared when switching to a null session + context("セッションが null に切り替わった場合", () => { + // hides FileChangesHeader + it("FileChangesHeader が非表示になること", async () => { + await setupWithDiffs([ + { file: "src/a.ts", before: "a", after: "b", additions: 1, deletions: 1 }, + ]); + expect(screen.getByText("File Changes")).toBeInTheDocument(); + + await sendExtMessage({ type: "activeSession", session: null as any }); + expect(screen.queryByText("File Changes")).not.toBeInTheDocument(); + }); + }); + + // session.diff SSE event updates the diff display + context("session.diff SSE イベントを受信した場合", () => { + // updates the file count + it("ファイル数が更新されること", async () => { + renderApp(); + await sendExtMessage({ type: "activeSession", session: createSession({ id: "s1" }) }); + + // 最初はファイル変更なし + expect(screen.queryByText("File Changes")).not.toBeInTheDocument(); + + // SSE event で差分を受信 + await sendExtMessage({ + type: "event", + event: { + type: "session.diff", + properties: { + sessionID: "s1", + diff: [ + { file: "src/a.ts", before: "x", after: "y", additions: 1, deletions: 1 }, + ], + }, + } as any, + }); + + expect(screen.getByText("File Changes")).toBeInTheDocument(); + expect(screen.getByText("1")).toBeInTheDocument(); + }); + }); + + // Inline diff is shown when file item is expanded + context("ファイルアイテムを展開した場合", () => { + // shows diff view with changed lines + it("変更行を含む差分ビューが表示されること", async () => { + await setupWithDiffs([ + { file: "src/main.ts", before: "const x = 1;", after: "const x = 2;", additions: 1, deletions: 1 }, + ]); + const user = userEvent.setup(); + + // ヘッダーバーを展開 + await user.click(screen.getByTitle("File changes")); + + // ファイルアイテムをクリックして差分を表示 + const fileHeader = document.querySelector(".fileHeader")!; + await user.click(fileHeader); + + expect(screen.getByTestId("diff-view")).toBeInTheDocument(); + }); + }); + + // Open diff editor button sends message + context("差分エディタを開くボタンをクリックした場合", () => { + // sends openDiffEditor message + it("openDiffEditor メッセージが送信されること", async () => { + await setupWithDiffs([ + { file: "src/main.ts", before: "before", after: "after", additions: 1, deletions: 1 }, + ]); + + const mockPostMessage = vi.mocked(postMessage); + mockPostMessage.mockClear(); + const user = userEvent.setup(); + + // ヘッダーバーを展開 + await user.click(screen.getByTitle("File changes")); + + // 外部リンクボタンをクリック + const openButton = document.querySelector(".openButton")!; + await user.click(openButton); + + const diffCall = mockPostMessage.mock.calls.find( + (call) => call[0]?.type === "openDiffEditor", + ); + expect(diffCall).toBeTruthy(); + }); + }); + + // Multiple files with summary stats + context("複数ファイルの変更がある場合", () => { + beforeEach(async () => { + await setupWithDiffs([ + { file: "src/a.ts", before: "a", after: "aa", additions: 5, deletions: 2 }, + { file: "src/b.ts", before: "", after: "new", additions: 10, deletions: 0 }, + { file: "src/c.ts", before: "c", after: "", additions: 0, deletions: 8 }, + ]); + }); + + // Shows total file count + it("合計ファイル数が表示されること", () => { + expect(screen.getByText("3")).toBeInTheDocument(); + }); + + // Shows all files when expanded + it("展開すると全ファイルが表示されること", async () => { + const user = userEvent.setup(); + await user.click(screen.getByTitle("File changes")); + + expect(screen.getByText("a.ts")).toBeInTheDocument(); + expect(screen.getByText("b.ts")).toBeInTheDocument(); + expect(screen.getByText("c.ts")).toBeInTheDocument(); + }); + }); +}); From 8a3444e6f5bc8aa88ed7a9fcffbcaec662bd2422 Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 03:23:08 +0900 Subject: [PATCH 7/7] style: apply Biome formatting fixes --- .../scenarios/15-file-changes.test.tsx | 20 +++++-------------- .../FileChangesHeader/FileChangesHeader.tsx | 5 ++++- .../organisms/ChatHeader/ChatHeader.tsx | 6 +----- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/webview/__tests__/scenarios/15-file-changes.test.tsx b/webview/__tests__/scenarios/15-file-changes.test.tsx index 9ce077d..e54e346 100644 --- a/webview/__tests__/scenarios/15-file-changes.test.tsx +++ b/webview/__tests__/scenarios/15-file-changes.test.tsx @@ -61,9 +61,7 @@ describe("ファイル変更差分表示", () => { // Expanding shows file details context("FileChangesHeader を展開した場合", () => { beforeEach(async () => { - await setupWithDiffs([ - { file: "src/app.ts", before: "old code", after: "new code", additions: 1, deletions: 1 }, - ]); + await setupWithDiffs([{ file: "src/app.ts", before: "old code", after: "new code", additions: 1, deletions: 1 }]); const user = userEvent.setup(); await user.click(screen.getByTitle("File changes")); }); @@ -96,9 +94,7 @@ describe("ファイル変更差分表示", () => { context("セッションが null に切り替わった場合", () => { // hides FileChangesHeader it("FileChangesHeader が非表示になること", async () => { - await setupWithDiffs([ - { file: "src/a.ts", before: "a", after: "b", additions: 1, deletions: 1 }, - ]); + await setupWithDiffs([{ file: "src/a.ts", before: "a", after: "b", additions: 1, deletions: 1 }]); expect(screen.getByText("File Changes")).toBeInTheDocument(); await sendExtMessage({ type: "activeSession", session: null as any }); @@ -123,9 +119,7 @@ describe("ファイル変更差分表示", () => { type: "session.diff", properties: { sessionID: "s1", - diff: [ - { file: "src/a.ts", before: "x", after: "y", additions: 1, deletions: 1 }, - ], + diff: [{ file: "src/a.ts", before: "x", after: "y", additions: 1, deletions: 1 }], }, } as any, }); @@ -159,9 +153,7 @@ describe("ファイル変更差分表示", () => { context("差分エディタを開くボタンをクリックした場合", () => { // sends openDiffEditor message it("openDiffEditor メッセージが送信されること", async () => { - await setupWithDiffs([ - { file: "src/main.ts", before: "before", after: "after", additions: 1, deletions: 1 }, - ]); + await setupWithDiffs([{ file: "src/main.ts", before: "before", after: "after", additions: 1, deletions: 1 }]); const mockPostMessage = vi.mocked(postMessage); mockPostMessage.mockClear(); @@ -174,9 +166,7 @@ describe("ファイル変更差分表示", () => { const openButton = document.querySelector(".openButton")!; await user.click(openButton); - const diffCall = mockPostMessage.mock.calls.find( - (call) => call[0]?.type === "openDiffEditor", - ); + const diffCall = mockPostMessage.mock.calls.find((call) => call[0]?.type === "openDiffEditor"); expect(diffCall).toBeTruthy(); }); }); diff --git a/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx b/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx index 079ee47..b187210 100644 --- a/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx +++ b/webview/components/molecules/FileChangesHeader/FileChangesHeader.tsx @@ -34,7 +34,10 @@ function getFileStatus(diff: FileDiff): "added" | "deleted" | "modified" { function FileChangeItem({ diff, onOpenDiffEditor, -}: { diff: FileDiff; onOpenDiffEditor: (filePath: string, before: string, after: string) => void }) { +}: { + diff: FileDiff; + onOpenDiffEditor: (filePath: string, before: string, after: string) => void; +}) { const t = useLocale(); const [expanded, setExpanded] = useState(false); const status = getFileStatus(diff); diff --git a/webview/components/organisms/ChatHeader/ChatHeader.tsx b/webview/components/organisms/ChatHeader/ChatHeader.tsx index 27cc538..bc0d30f 100644 --- a/webview/components/organisms/ChatHeader/ChatHeader.tsx +++ b/webview/components/organisms/ChatHeader/ChatHeader.tsx @@ -10,11 +10,7 @@ type Props = { onToggleSessionList: () => void; }; -export function ChatHeader({ - activeSession, - onNewSession, - onToggleSessionList, -}: Props) { +export function ChatHeader({ activeSession, onNewSession, onToggleSessionList }: Props) { const t = useLocale(); return (