diff --git a/webview/__tests__/components/atoms/ListItem.test.tsx b/webview/__tests__/components/atoms/ListItem.test.tsx
index 05b8eaa..559a812 100644
--- a/webview/__tests__/components/atoms/ListItem.test.tsx
+++ b/webview/__tests__/components/atoms/ListItem.test.tsx
@@ -55,4 +55,22 @@ describe("ListItem", () => {
expect(container.querySelector(".root")?.classList.contains("custom")).toBe(true);
});
});
+
+ // when focused is true
+ context("focused が true の場合", () => {
+ // has data-focused attribute
+ it("data-focused 属性を持つこと", () => {
+ const { container } = render();
+ expect(container.querySelector(".root")?.getAttribute("data-focused")).toBe("true");
+ });
+ });
+
+ // when focused is false or undefined
+ context("focused が false または未指定の場合", () => {
+ // does not have data-focused attribute
+ it("data-focused 属性を持たないこと", () => {
+ const { container } = render();
+ expect(container.querySelector(".root")?.hasAttribute("data-focused")).toBe(false);
+ });
+ });
});
diff --git a/webview/__tests__/components/molecules/AgentPopup.test.tsx b/webview/__tests__/components/molecules/AgentPopup.test.tsx
index 1c81fa8..28a3e15 100644
--- a/webview/__tests__/components/molecules/AgentPopup.test.tsx
+++ b/webview/__tests__/components/molecules/AgentPopup.test.tsx
@@ -23,7 +23,7 @@ describe("AgentPopup", () => {
// renders agent names
it("エージェント名を表示すること", () => {
const { container } = render(
- ,
+ ,
);
const titles = container.querySelectorAll(".title");
expect(titles[0]?.textContent).toBe("coder");
@@ -33,7 +33,7 @@ describe("AgentPopup", () => {
// renders agent descriptions
it("エージェントの説明を表示すること", () => {
const { container } = render(
- ,
+ ,
);
const descriptions = container.querySelectorAll(".description");
expect(descriptions[0]?.textContent).toBe("Coding agent");
@@ -44,7 +44,7 @@ describe("AgentPopup", () => {
const onSelect = vi.fn();
const user = userEvent.setup();
const { container } = render(
- ,
+ ,
);
const items = container.querySelectorAll(".root > div");
await user.click(items[0]!);
@@ -52,14 +52,18 @@ describe("AgentPopup", () => {
});
});
- // when no agents available
- context("エージェントがない場合", () => {
- // shows empty message
- it("空メッセージを表示すること", () => {
+ // when focusedIndex highlights a specific agent
+ context("focusedIndex が指定された場合", () => {
+ const agents = [createAgent("coder", "Coding agent"), createAgent("researcher", "Research agent")];
+
+ // applies data-focused to the correct item
+ it("対応するアイテムに data-focused 属性が付与されること", () => {
const { container } = render(
- ,
+ ,
);
- expect(container.querySelector(".empty")?.textContent).toBe("No agents available");
+ const items = container.querySelectorAll(".root > div");
+ expect(items[0]?.getAttribute("data-focused")).toBe("true");
+ expect(items[1]?.hasAttribute("data-focused")).toBe(false);
});
});
});
diff --git a/webview/__tests__/components/molecules/HashFilePopup.test.tsx b/webview/__tests__/components/molecules/HashFilePopup.test.tsx
index 7d63821..7bd97d1 100644
--- a/webview/__tests__/components/molecules/HashFilePopup.test.tsx
+++ b/webview/__tests__/components/molecules/HashFilePopup.test.tsx
@@ -16,7 +16,7 @@ describe("HashFilePopup", () => {
// renders file items
it("ファイルアイテムをレンダリングすること", () => {
const { container } = render(
- ,
+ ,
);
expect(container.querySelectorAll("[data-testid='hash-popup'] > div")).toHaveLength(2);
});
@@ -25,7 +25,7 @@ describe("HashFilePopup", () => {
it("アイテムクリックで onAddFile が呼ばれること", () => {
const onAddFile = vi.fn();
const { container } = render(
- ,
+ ,
);
fireEvent.click(container.querySelector("[data-testid='hash-popup'] > div")!);
expect(onAddFile).toHaveBeenCalledWith(file1);
@@ -36,8 +36,23 @@ describe("HashFilePopup", () => {
context("ファイル候補がない場合", () => {
// renders empty message
it("空メッセージを表示すること", () => {
- const { container } = render();
+ const { container } = render(
+ ,
+ );
expect(container.querySelector(".empty")).toBeInTheDocument();
});
});
+
+ // when focusedIndex highlights a specific item
+ context("focusedIndex が指定された場合", () => {
+ // applies data-focused to the correct item
+ it("対応するアイテムに data-focused 属性が付与されること", () => {
+ const { container } = render(
+ ,
+ );
+ const items = container.querySelectorAll("[data-testid='hash-popup'] > div");
+ expect(items[0]?.hasAttribute("data-focused")).toBe(false);
+ expect(items[1]?.getAttribute("data-focused")).toBe("true");
+ });
+ });
});
diff --git a/webview/__tests__/scenarios/21-popup-tab-select.test.tsx b/webview/__tests__/scenarios/21-popup-tab-select.test.tsx
new file mode 100644
index 0000000..af3e5a5
--- /dev/null
+++ b/webview/__tests__/scenarios/21-popup-tab-select.test.tsx
@@ -0,0 +1,262 @@
+import { screen } 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";
+
+/** テスト用エージェントデータ */
+const testAgents = [
+ {
+ name: "coder",
+ description: "Coding subagent",
+ mode: "subagent",
+ builtIn: true,
+ permission: { edit: "ask", bash: {} },
+ tools: {},
+ options: {},
+ },
+ {
+ name: "explorer",
+ description: "Read-only subagent",
+ mode: "subagent",
+ builtIn: true,
+ permission: { edit: "deny", bash: {} },
+ tools: {},
+ options: {},
+ },
+] as any;
+
+/** ファイル候補付きセットアップ */
+async function setupWithFiles() {
+ renderApp();
+ await sendExtMessage({ type: "activeSession", session: createSession({ id: "s1" }) });
+ await sendExtMessage({
+ type: "openEditors",
+ files: [
+ { filePath: "src/main.ts", fileName: "main.ts" },
+ { filePath: "src/utils.ts", fileName: "utils.ts" },
+ ],
+ });
+ await sendExtMessage({
+ type: "workspaceFiles",
+ files: [
+ { filePath: "src/main.ts", fileName: "main.ts" },
+ { filePath: "src/utils.ts", fileName: "utils.ts" },
+ { filePath: "src/config.ts", fileName: "config.ts" },
+ ],
+ });
+ await sendExtMessage({ type: "agents", agents: testAgents });
+ vi.mocked(postMessage).mockClear();
+}
+
+// Tab selection in inline popups
+describe("ポップアップの Tab 選択", () => {
+ beforeEach(async () => {
+ await setupWithFiles();
+ });
+
+ // # popup: Tab moves focus through items
+ context("# ポップアップで Tab を押した場合", () => {
+ // first item gets focused
+ it("先頭のアイテムにフォーカスが当たること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{Tab}");
+ const popup = document.querySelector("[data-testid='hash-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ expect(items?.[0]?.getAttribute("data-focused")).toBe("true");
+ });
+
+ // Tab again moves to second item
+ it("もう一度 Tab を押すと 2 番目のアイテムにフォーカスが移ること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{Tab}");
+ await user.keyboard("{Tab}");
+ const popup = document.querySelector("[data-testid='hash-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ expect(items?.[1]?.getAttribute("data-focused")).toBe("true");
+ });
+ });
+
+ // # popup: Tab + Enter selects the focused item
+ context("# ポップアップで Tab でフォーカス後 Enter で確定した場合", () => {
+ // file is attached and popup closes
+ it("ファイルが添付されポップアップが閉じること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{Tab}");
+ await user.keyboard("{Enter}");
+ // ポップアップが閉じる
+ expect(document.querySelector("[data-testid='hash-popup']")).toBeFalsy();
+ // ファイルチップが表示される
+ expect(document.querySelectorAll(".chip").length).toBe(1);
+ });
+ });
+
+ // # popup: ArrowDown moves focus
+ context("# ポップアップで ↓ キーを押した場合", () => {
+ // second item gets focused
+ it("次のアイテムにフォーカスが移ること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{ArrowDown}");
+ const popup = document.querySelector("[data-testid='hash-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ expect(items?.[0]?.getAttribute("data-focused")).toBe("true");
+ });
+
+ // pressing ArrowDown again moves to second item
+ it("もう一度 ↓ を押すと2番目のアイテムにフォーカスが移ること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{ArrowDown}");
+ await user.keyboard("{ArrowDown}");
+ const popup = document.querySelector("[data-testid='hash-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ expect(items?.[1]?.getAttribute("data-focused")).toBe("true");
+ });
+ });
+
+ // # popup: ArrowUp wraps to last item
+ context("# ポップアップで ↑ キーを押した場合", () => {
+ // wraps to last item
+ it("末尾のアイテムにフォーカスが移ること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{ArrowUp}");
+ const popup = document.querySelector("[data-testid='hash-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ const lastItem = items?.[items.length - 1];
+ expect(lastItem?.getAttribute("data-focused")).toBe("true");
+ });
+ });
+
+ // # popup: Enter on focused item selects it
+ context("# ポップアップでフォーカス済みアイテムで Enter を押した場合", () => {
+ // file is attached and popup closes
+ it("ファイルが添付されポップアップが閉じること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{ArrowDown}");
+ await user.keyboard("{Enter}");
+ // ポップアップが閉じる
+ expect(document.querySelector("[data-testid='hash-popup']")).toBeFalsy();
+ // ファイルチップが表示される
+ expect(document.querySelectorAll(".chip").length).toBe(1);
+ });
+ });
+
+ // # popup: Enter without focus sends message
+ context("# ポップアップ表示中にフォーカスなしで Enter を押した場合", () => {
+ // sends the message (existing behavior)
+ it("メッセージが送信されること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "hello #");
+ await user.keyboard("{Enter}");
+ expect(postMessage).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "sendMessage",
+ text: "hello #",
+ }),
+ );
+ });
+ });
+
+ // # popup: query change resets focus
+ context("# ポップアップでクエリを変更した場合", () => {
+ // focus resets
+ it("フォーカスがリセットされること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "#");
+ await user.keyboard("{ArrowDown}");
+ // フォーカスが当たっている
+ let popup = document.querySelector("[data-testid='hash-popup']");
+ expect(popup?.querySelector("[data-focused]")).toBeTruthy();
+ // クエリを入力するとフォーカスリセット
+ await user.type(textarea, "m");
+ popup = document.querySelector("[data-testid='hash-popup']");
+ expect(popup?.querySelector("[data-focused]")).toBeFalsy();
+ });
+ });
+
+ // @ popup: Tab moves focus through agents
+ context("@ ポップアップで Tab を押した場合", () => {
+ // first agent gets focused
+ it("先頭のエージェントにフォーカスが当たること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "@");
+ await user.keyboard("{Tab}");
+ const popup = document.querySelector("[data-testid='agent-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ expect(items?.[0]?.getAttribute("data-focused")).toBe("true");
+ });
+
+ // Tab again moves to second agent
+ it("もう一度 Tab を押すと 2 番目のエージェントにフォーカスが移ること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "@");
+ await user.keyboard("{Tab}");
+ await user.keyboard("{Tab}");
+ const popup = document.querySelector("[data-testid='agent-popup']");
+ const items = popup?.querySelectorAll(":scope > div");
+ expect(items?.[1]?.getAttribute("data-focused")).toBe("true");
+ });
+ });
+
+ // @ popup: Tab + Enter selects the focused agent
+ context("@ ポップアップで Tab でフォーカス後 Enter で確定した場合", () => {
+ // agent is selected and popup closes
+ it("エージェントが選択されポップアップが閉じること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "@");
+ await user.keyboard("{Tab}");
+ await user.keyboard("{Enter}");
+ expect(screen.queryByTestId("agent-popup")).not.toBeInTheDocument();
+ expect(screen.getByText("@coder")).toBeInTheDocument();
+ });
+ });
+
+ // @ popup: Enter on focused agent selects it
+ context("@ ポップアップでフォーカス済みエージェントで Enter を押した場合", () => {
+ // agent is selected
+ it("エージェントが選択されること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "@");
+ await user.keyboard("{ArrowDown}");
+ await user.keyboard("{Enter}");
+ expect(screen.queryByTestId("agent-popup")).not.toBeInTheDocument();
+ expect(screen.getByText("@coder")).toBeInTheDocument();
+ });
+ });
+
+ // @ popup: ArrowDown/ArrowUp navigation
+ context("@ ポップアップで ↓↑ キーで移動した場合", () => {
+ // ArrowDown then ArrowUp returns to first
+ it("↓ で 2 番目、↑ で 1 番目に戻ること", async () => {
+ const user = userEvent.setup();
+ const textarea = screen.getByPlaceholderText("Ask OpenCode... (type # to attach files)");
+ await user.type(textarea, "@");
+ await user.keyboard("{ArrowDown}");
+ await user.keyboard("{ArrowDown}");
+ const popup = document.querySelector("[data-testid='agent-popup']");
+ expect(popup?.querySelectorAll(":scope > div")[1]?.getAttribute("data-focused")).toBe("true");
+ await user.keyboard("{ArrowUp}");
+ expect(popup?.querySelectorAll(":scope > div")[0]?.getAttribute("data-focused")).toBe("true");
+ });
+ });
+});
diff --git a/webview/components/atoms/ListItem/ListItem.module.css b/webview/components/atoms/ListItem/ListItem.module.css
index c16fedc..edc60e7 100644
--- a/webview/components/atoms/ListItem/ListItem.module.css
+++ b/webview/components/atoms/ListItem/ListItem.module.css
@@ -6,7 +6,8 @@
font-size: 12px;
}
-.root:hover {
+.root:hover,
+.root[data-focused] {
background-color: var(--vscode-list-hoverBackground);
}
diff --git a/webview/components/atoms/ListItem/ListItem.tsx b/webview/components/atoms/ListItem/ListItem.tsx
index 60346e9..60298d1 100644
--- a/webview/components/atoms/ListItem/ListItem.tsx
+++ b/webview/components/atoms/ListItem/ListItem.tsx
@@ -5,16 +5,17 @@ type Props = {
description?: string;
onClick?: () => void;
className?: string;
+ focused?: boolean;
};
/**
* タイトル + 説明テキストを縦積みで表示する汎用リスト行。
*/
-export function ListItem({ title, description, onClick, className }: Props) {
+export function ListItem({ title, description, onClick, className, focused }: Props) {
const classes = [styles.root, className].filter(Boolean).join(" ");
return (
-
+
{title}
{description && {description}}
diff --git a/webview/components/molecules/AgentPopup/AgentPopup.tsx b/webview/components/molecules/AgentPopup/AgentPopup.tsx
index 87958b0..948f47b 100644
--- a/webview/components/molecules/AgentPopup/AgentPopup.tsx
+++ b/webview/components/molecules/AgentPopup/AgentPopup.tsx
@@ -7,20 +7,22 @@ type Props = {
agents: Agent[];
onSelectAgent: (agent: Agent) => void;
agentPopupRef: React.RefObject
;
+ focusedIndex: number;
};
-export function AgentPopup({ agents, onSelectAgent, agentPopupRef }: Props) {
+export function AgentPopup({ agents, onSelectAgent, agentPopupRef, focusedIndex }: Props) {
const t = useLocale();
return (
{agents.length > 0 ? (
- agents.map((agent) => (
+ agents.map((agent, i) => (
onSelectAgent(agent)}
+ focused={i === focusedIndex}
/>
))
) : (
diff --git a/webview/components/molecules/HashFilePopup/HashFilePopup.tsx b/webview/components/molecules/HashFilePopup/HashFilePopup.tsx
index 3634909..1ecb179 100644
--- a/webview/components/molecules/HashFilePopup/HashFilePopup.tsx
+++ b/webview/components/molecules/HashFilePopup/HashFilePopup.tsx
@@ -7,20 +7,22 @@ type Props = {
hashFiles: FileAttachment[];
onAddFile: (file: FileAttachment) => void;
hashPopupRef: React.RefObject;
+ focusedIndex: number;
};
-export function HashFilePopup({ hashFiles, onAddFile, hashPopupRef }: Props) {
+export function HashFilePopup({ hashFiles, onAddFile, hashPopupRef, focusedIndex }: Props) {
const t = useLocale();
return (
{hashFiles.length > 0 ? (
- hashFiles.map((file) => (
+ hashFiles.map((file, i) => (
onAddFile(file)}
+ focused={i === focusedIndex}
/>
))
) : (
diff --git a/webview/components/organisms/InputArea/InputArea.tsx b/webview/components/organisms/InputArea/InputArea.tsx
index 0a2ab81..0494bd8 100644
--- a/webview/components/organisms/InputArea/InputArea.tsx
+++ b/webview/components/organisms/InputArea/InputArea.tsx
@@ -83,6 +83,9 @@ export function InputArea({
});
const [atQuery, setAtQuery] = useState("");
const [selectedAgent, setSelectedAgent] = useState(null);
+ // ポップアップ内のフォーカス位置(-1 = フォーカスなし)
+ const [hashFocusedIndex, setHashFocusedIndex] = useState(-1);
+ const [atFocusedIndex, setAtFocusedIndex] = useState(-1);
const textareaRef = useRef(null);
const composingRef = useRef(false);
const filePickerRef = useRef(null);
@@ -225,20 +228,87 @@ export function InputArea({
}
}, [text, attachedFiles, onSend, onShellExecute, selectedAgent?.name]);
+ // # トリガーのファイル候補
+ const hashFiles = hashQuery
+ ? workspaceFiles
+ .filter(
+ (f) =>
+ f.fileName.toLowerCase().includes(hashQuery.toLowerCase()) ||
+ f.filePath.toLowerCase().includes(hashQuery.toLowerCase()),
+ )
+ .filter((f) => !attachedFiles.some((a) => a.filePath === f.filePath))
+ .slice(0, 10)
+ : [...openEditors, ...workspaceFiles.filter((f) => !openEditors.some((o) => o.filePath === f.filePath))]
+ .filter((f) => !attachedFiles.some((a) => a.filePath === f.filePath))
+ .slice(0, 10);
+
+ // @ トリガーのエージェント候補(サブエージェントのみ表示)
+ const subagents = agents.filter((a) => a.mode === "subagent" || a.mode === "all");
+ const filteredAgents = atQuery
+ ? subagents.filter((a) => a.name.toLowerCase().includes(atQuery.toLowerCase())).slice(0, 10)
+ : subagents.slice(0, 10);
+
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
// Escape で # ポップアップを閉じる
if (e.key === "Escape" && hashTrigger.active) {
setHashTrigger({ active: false, startIndex: -1 });
setHashQuery("");
+ setHashFocusedIndex(-1);
return;
}
// Escape で @ ポップアップを閉じる
if (e.key === "Escape" && atTrigger.active) {
setAtTrigger({ active: false, startIndex: -1 });
setAtQuery("");
+ setAtFocusedIndex(-1);
return;
}
+
+ // # ポップアップ表示中の Tab / ↑ / ↓ / Enter ナビゲーション
+ if (hashTrigger.active && hashFiles.length > 0) {
+ // Tab / ↓ はフォーカスを次の項目に移動する
+ if (e.key === "Tab" || e.key === "ArrowDown") {
+ e.preventDefault();
+ setHashFocusedIndex((prev) => (prev + 1) % hashFiles.length);
+ return;
+ }
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
+ setHashFocusedIndex((prev) => (prev <= 0 ? hashFiles.length - 1 : prev - 1));
+ return;
+ }
+ // Enter でフォーカス中の項目を確定する
+ if (e.key === "Enter" && !e.shiftKey && !composingRef.current && hashFocusedIndex >= 0) {
+ e.preventDefault();
+ addFile(hashFiles[hashFocusedIndex]);
+ setHashFocusedIndex(-1);
+ return;
+ }
+ }
+
+ // @ ポップアップ表示中の Tab / ↑ / ↓ / Enter ナビゲーション
+ if (atTrigger.active && filteredAgents.length > 0) {
+ // Tab / ↓ はフォーカスを次の項目に移動する
+ if (e.key === "Tab" || e.key === "ArrowDown") {
+ e.preventDefault();
+ setAtFocusedIndex((prev) => (prev + 1) % filteredAgents.length);
+ return;
+ }
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
+ setAtFocusedIndex((prev) => (prev <= 0 ? filteredAgents.length - 1 : prev - 1));
+ return;
+ }
+ // Enter でフォーカス中の項目を確定する
+ if (e.key === "Enter" && !e.shiftKey && !composingRef.current && atFocusedIndex >= 0) {
+ e.preventDefault();
+ selectAgent(filteredAgents[atFocusedIndex]);
+ setAtFocusedIndex(-1);
+ return;
+ }
+ }
+
// IME 変換中は送信しない
if (e.key === "Enter" && !e.shiftKey && !composingRef.current) {
e.preventDefault();
@@ -247,15 +317,28 @@ export function InputArea({
if (hashTrigger.active) {
setHashTrigger({ active: false, startIndex: -1 });
setHashQuery("");
+ setHashFocusedIndex(-1);
}
if (atTrigger.active) {
setAtTrigger({ active: false, startIndex: -1 });
setAtQuery("");
+ setAtFocusedIndex(-1);
}
handleSend();
}
},
- [handleSend, isBusy, hashTrigger.active, atTrigger.active],
+ [
+ handleSend,
+ isBusy,
+ hashTrigger.active,
+ atTrigger.active,
+ hashFocusedIndex,
+ atFocusedIndex,
+ hashFiles,
+ filteredAgents,
+ addFile,
+ selectAgent,
+ ],
);
const handleTextChange = useCallback(
@@ -296,8 +379,11 @@ export function InputArea({
if (/[\s]/.test(queryPart) || cursorPos <= hashTrigger.startIndex) {
setHashTrigger({ active: false, startIndex: -1 });
setHashQuery("");
+ setHashFocusedIndex(-1);
} else {
setHashQuery(queryPart);
+ // クエリ変更時にフォーカスをリセットする
+ setHashFocusedIndex(-1);
}
}
@@ -307,8 +393,11 @@ export function InputArea({
if (/[\s]/.test(queryPart) || cursorPos <= atTrigger.startIndex) {
setAtTrigger({ active: false, startIndex: -1 });
setAtQuery("");
+ setAtFocusedIndex(-1);
} else {
setAtQuery(queryPart);
+ // クエリ変更時にフォーカスをリセットする
+ setAtFocusedIndex(-1);
}
}
},
@@ -329,32 +418,12 @@ export function InputArea({
(f) => !attachedFiles.some((a) => a.filePath === f.filePath),
);
- // # トリガーのファイル候補
- const hashFiles = hashQuery
- ? workspaceFiles
- .filter(
- (f) =>
- f.fileName.toLowerCase().includes(hashQuery.toLowerCase()) ||
- f.filePath.toLowerCase().includes(hashQuery.toLowerCase()),
- )
- .filter((f) => !attachedFiles.some((a) => a.filePath === f.filePath))
- .slice(0, 10)
- : [...openEditors, ...workspaceFiles.filter((f) => !openEditors.some((o) => o.filePath === f.filePath))]
- .filter((f) => !attachedFiles.some((a) => a.filePath === f.filePath))
- .slice(0, 10);
-
// 現在アクティブなエディタファイル (リストの先頭)
const activeEditorFile = openEditors.length > 0 ? openEditors[0] : null;
const isActiveAttached = activeEditorFile
? attachedFiles.some((f) => f.filePath === activeEditorFile.filePath)
: false;
- // @ トリガーのエージェント候補(サブエージェントのみ表示)
- const subagents = agents.filter((a) => a.mode === "subagent" || a.mode === "all");
- const filteredAgents = atQuery
- ? subagents.filter((a) => a.name.toLowerCase().includes(atQuery.toLowerCase())).slice(0, 10)
- : subagents.slice(0, 10);
-
return (
@@ -422,11 +491,21 @@ export function InputArea({
/>
{/* # トリガー ファイル候補ポップアップ */}
{hashTrigger.active && (
-
+
)}
{/* @ トリガー エージェント候補ポップアップ */}
{atTrigger.active && (
-
+
)}