From db2f43c1777f8cc9fd456642800c95566cbb41f9 Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 23:35:06 +0900 Subject: [PATCH 1/3] feat: add activeEditor message for real-time active editor tracking Add onDidChangeActiveTextEditor listener in extension host to push activeEditor messages to webview. Send initial activeEditor on ready. Add activeEditor type to ExtToWebviewMessage in both extension and webview protocol definitions. --- src/chat-view-provider.ts | 21 +++++++++++++++++++++ webview/vscode-api.ts | 1 + 2 files changed, 22 insertions(+) diff --git a/src/chat-view-provider.ts b/src/chat-view-provider.ts index ac3e9ca..ad03bf7 100644 --- a/src/chat-view-provider.ts +++ b/src/chat-view-provider.ts @@ -35,6 +35,7 @@ export type ExtToWebviewMessage = configModel?: string; } | { type: "openEditors"; files: FileAttachment[] } + | { type: "activeEditor"; file: FileAttachment | null } | { type: "workspaceFiles"; files: FileAttachment[] } | { type: "contextUsage"; usage: { inputTokens: number; contextLimit: number } } | { type: "toolConfig"; paths: OpenCodePath } @@ -124,6 +125,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.connection.onEvent((event) => { this.postMessage({ type: "event", event }); }); + + // アクティブエディタが変わるたびに Webview に通知する + vscode.window.onDidChangeActiveTextEditor((editor) => { + this.postMessage({ type: "activeEditor", file: this.getActiveEditorFile(editor) }); + }); } private async handleWebviewMessage(message: WebviewToExtMessage): Promise { @@ -165,6 +171,8 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { configModel, }); this.postMessage({ type: "toolConfig", paths }); + // 初期アクティブエディタを送信する + this.postMessage({ type: "activeEditor", file: this.getActiveEditorFile(vscode.window.activeTextEditor) }); break; } case "sendMessage": { @@ -420,6 +428,19 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } } + /** アクティブなテキストエディタから FileAttachment を生成する。エディタがない場合は null を返す。 */ + private getActiveEditorFile(editor: vscode.TextEditor | undefined): FileAttachment | null { + if (!editor) return null; + const uri = editor.document.uri; + // 出力パネルや設定画面など、file スキーム以外は対象外 + if (uri.scheme !== "file") return null; + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri; + const relativePath = workspaceFolder + ? path.relative(workspaceFolder.fsPath, uri.fsPath) + : path.basename(uri.fsPath); + return { filePath: relativePath, fileName: path.basename(uri.fsPath) }; + } + private postMessage(message: ExtToWebviewMessage): void { this.view?.webview.postMessage(message); } diff --git a/webview/vscode-api.ts b/webview/vscode-api.ts index d49c9cf..4e404ba 100644 --- a/webview/vscode-api.ts +++ b/webview/vscode-api.ts @@ -61,6 +61,7 @@ export type ExtToWebviewMessage = configModel?: string; } | { type: "openEditors"; files: FileAttachment[] } + | { type: "activeEditor"; file: FileAttachment | null } | { type: "workspaceFiles"; files: FileAttachment[] } | { type: "contextUsage"; usage: { inputTokens: number; contextLimit: number } } | { type: "toolConfig"; paths: { home: string; config: string; state: string; directory: string } } From 5fc045fc473acd0a97c7df9b76624df971fff88d Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 23:35:11 +0900 Subject: [PATCH 2/3] feat: use activeEditorFile from extension for quick-add button Replace openEditors[0] with activeEditorFile state in InputArea. App.tsx handles activeEditor messages and passes the file to InputArea via props. Quick-add button now reflects the actual active editor. --- webview/App.tsx | 5 +++++ webview/components/organisms/InputArea/InputArea.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/webview/App.tsx b/webview/App.tsx index 8206b01..c395727 100644 --- a/webview/App.tsx +++ b/webview/App.tsx @@ -31,6 +31,7 @@ export function App() { // Extension Host → Webview メッセージでのみ更新される単純なステート const [openEditors, setOpenEditors] = useState([]); + const [activeEditorFile, setActiveEditorFile] = useState(null); const [workspaceFiles, setWorkspaceFiles] = useState([]); const [todos, setTodos] = useState([]); const [childSessions, setChildSessions] = useState([]); @@ -136,6 +137,9 @@ export function App() { case "openEditors": setOpenEditors(data.files); break; + case "activeEditor": + setActiveEditorFile(data.file); + break; case "workspaceFiles": setWorkspaceFiles(data.files); break; @@ -487,6 +491,7 @@ export function App() { selectedModel={prov.selectedModel} onModelSelect={prov.handleModelSelect} openEditors={openEditors} + activeEditorFile={activeEditorFile} workspaceFiles={workspaceFiles} inputTokens={msg.inputTokens} contextLimit={prov.contextLimit} diff --git a/webview/components/organisms/InputArea/InputArea.tsx b/webview/components/organisms/InputArea/InputArea.tsx index 0494bd8..391b2e6 100644 --- a/webview/components/organisms/InputArea/InputArea.tsx +++ b/webview/components/organisms/InputArea/InputArea.tsx @@ -26,6 +26,7 @@ type Props = { selectedModel: { providerID: string; modelID: string } | null; onModelSelect: (model: { providerID: string; modelID: string }) => void; openEditors: FileAttachment[]; + activeEditorFile: FileAttachment | null; workspaceFiles: FileAttachment[]; inputTokens: number; contextLimit: number; @@ -51,6 +52,7 @@ export function InputArea({ selectedModel, onModelSelect, openEditors, + activeEditorFile, workspaceFiles, inputTokens, contextLimit, @@ -418,8 +420,6 @@ export function InputArea({ (f) => !attachedFiles.some((a) => a.filePath === f.filePath), ); - // 現在アクティブなエディタファイル (リストの先頭) - const activeEditorFile = openEditors.length > 0 ? openEditors[0] : null; const isActiveAttached = activeEditorFile ? attachedFiles.some((f) => f.filePath === activeEditorFile.filePath) : false; From bb34c4484f3d228d40d43991e29a27851cc82b7c Mon Sep 17 00:00:00 2001 From: ktmage Date: Sun, 1 Mar 2026 23:35:16 +0900 Subject: [PATCH 3/3] test: add activeEditor message tests for quick-add button Update setupWithFiles to send activeEditor message. Add tests for real-time quick-add button update via activeEditor message and null activeEditor hiding the quick-add button. --- .../scenarios/07-file-context.test.tsx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/webview/__tests__/scenarios/07-file-context.test.tsx b/webview/__tests__/scenarios/07-file-context.test.tsx index 17b12c5..5f24d2d 100644 --- a/webview/__tests__/scenarios/07-file-context.test.tsx +++ b/webview/__tests__/scenarios/07-file-context.test.tsx @@ -28,6 +28,12 @@ async function setupWithFiles() { ], }); + // アクティブエディタを設定 + await sendExtMessage({ + type: "activeEditor", + file: { filePath: "src/main.ts", fileName: "main.ts" }, + }); + vi.mocked(postMessage).mockClear(); } @@ -169,12 +175,12 @@ describe("ファイルコンテキスト", () => { ); }); - // Quick-add button attaches the first open editor file - it("アクティブエディタの quick-add ボタンで先頭ファイルが添付されること", async () => { + // Quick-add button attaches the active editor file + it("アクティブエディタの quick-add ボタンでファイルが添付されること", async () => { await setupWithFiles(); const user = userEvent.setup(); - // quick-add ボタン(最初の openEditor ファイル: main.ts)が表示される + // quick-add ボタン(アクティブエディタ: main.ts)が表示される const quickAdd = screen.getByTitle("Add src/main.ts"); expect(quickAdd).toBeInTheDocument(); @@ -230,4 +236,36 @@ describe("ファイルコンテキスト", () => { // スペースを入力したのでポップアップが閉じる expect(document.querySelector("[data-testid='hash-popup']")).toBeFalsy(); }); + + // activeEditor message updates the quick-add button in real-time + it("activeEditor メッセージで quick-add ボタンがリアルタイムに更新されること", async () => { + await setupWithFiles(); + + // 初期状態: main.ts が表示されている + expect(screen.getByTitle("Add src/main.ts")).toBeInTheDocument(); + + // アクティブエディタを config.ts に切り替え + await sendExtMessage({ + type: "activeEditor", + file: { filePath: "src/config.ts", fileName: "config.ts" }, + }); + + // quick-add ボタンが config.ts に切り替わる + expect(screen.getByTitle("Add src/config.ts")).toBeInTheDocument(); + expect(screen.queryByTitle("Add src/main.ts")).not.toBeInTheDocument(); + }); + + // activeEditor null hides the quick-add button + it("activeEditor が null の場合 quick-add ボタンが非表示になること", async () => { + await setupWithFiles(); + + // 初期状態: main.ts が表示されている + expect(screen.getByTitle("Add src/main.ts")).toBeInTheDocument(); + + // アクティブエディタを null に + await sendExtMessage({ type: "activeEditor", file: null }); + + // quick-add ボタンが消える + expect(document.querySelector(".fileButton")).not.toBeInTheDocument(); + }); });