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/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/__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(); + }); }); 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; 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 } }