Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ tests/
| `src/app/api/worktrees/[id]/git/log/route.ts` | Gitコミット履歴取得API(Issue #447) |
| `src/app/api/worktrees/[id]/git/show/[commitHash]/route.ts` | Gitコミット変更ファイル一覧API(Issue #447) |
| `src/app/api/worktrees/[id]/git/diff/route.ts` | Gitファイルdiff取得API(Issue #447) |
| `src/app/api/worktrees/[id]/special-keys/route.ts` | 特殊キー送信API(Up/Down/Enter/Escape、6層防御)(Issue #473) |
| `src/components/worktree/NavigationButtons.tsx` | OpenCode TUI選択リストナビゲーションボタン(Issue #473) |
| `src/app/api/worktrees/[id]/special-keys/route.ts` | 特殊キー送信API(Up/Down/Left/Right/Enter/Escape、6層防御)(Issue #473, #592) |
| `src/components/worktree/NavigationButtons.tsx` | OpenCode TUI選択リストナビゲーションボタン、Left/Right対応(Issue #473, #592) |
| `src/cli/utils/api-client.ts` | CLI用HTTPクライアント(認証トークン解決・エラー分類・ApiClient/ApiError)(Issue #518) |
| `src/cli/utils/command-helpers.ts` | CLI共通ヘルパー(TOKEN_WARNING定数・handleCommandError統一エラーハンドラ)(Issue #518) |
| `src/cli/types/api-responses.ts` | CLI側APIレスポンス型定義(WorktreeListResponse, CurrentOutputResponse, PromptResponseResult等)(Issue #518) |
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/worktrees/[id]/special-keys/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Special Keys API endpoint
* Sends navigation keys (Up/Down/Enter/Escape/Tab/BTab) to tmux sessions
* for TUI interaction (e.g., OpenCode selection lists).
* Sends navigation keys (Up/Down/Left/Right/Enter/Escape/Tab/BTab) to tmux sessions
* for TUI interaction (e.g., OpenCode selection lists, Copilot reasoning effort).
*
* Issue #473: Multi-layer defense following terminal/route.ts pattern.
* [DR1-001] Validation structure mirrors terminal/route.ts.
Expand Down
10 changes: 8 additions & 2 deletions src/components/worktree/NavigationButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/**
* NavigationButtons component for TUI selection list navigation.
* Issue #473: Provides Up/Down/Enter/Escape buttons for OpenCode TUI interaction.
* Issue #592: Added Left/Right buttons for Copilot reasoning effort adjustment.
*
* Touch targets: minimum 44x44px for mobile accessibility.
* Keyboard: Arrow keys intercepted only when component has focus.
Expand All @@ -11,6 +12,7 @@

import { useCallback, useState, type KeyboardEvent } from 'react';
import type { CLIToolType } from '@/lib/cli-tools/types';
import type { NavigationKey } from '@/lib/tmux/tmux';

export interface NavigationButtonsProps {
worktreeId: string;
Expand All @@ -20,12 +22,14 @@ export interface NavigationButtonsProps {
}

/** Navigation button configuration */
const NAVIGATION_BUTTONS = [
const NAVIGATION_BUTTONS: ReadonlyArray<{ key: NavigationKey; label: string; ariaLabel: string }> = [
{ key: 'Left', label: '\u25C0', ariaLabel: 'Left' },
{ key: 'Up', label: '\u25B2', ariaLabel: 'Up' },
{ key: 'Down', label: '\u25BC', ariaLabel: 'Down' },
{ key: 'Right', label: '\u25B6', ariaLabel: 'Right' },
{ key: 'Enter', label: '\u21B5', ariaLabel: 'Enter' },
{ key: 'Escape', label: 'Esc', ariaLabel: 'Escape' },
] as const;
];

export function NavigationButtons({ worktreeId, cliToolId, onKeysSent }: NavigationButtonsProps) {
const [activeKey, setActiveKey] = useState<string | null>(null);
Expand All @@ -52,8 +56,10 @@ export function NavigationButtons({ worktreeId, cliToolId, onKeysSent }: Navigat

const handleKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
const keyMap: Record<string, string> = {
ArrowLeft: 'Left',
ArrowUp: 'Up',
ArrowDown: 'Down',
ArrowRight: 'Right',
Enter: 'Enter',
Escape: 'Escape',
};
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tmux/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ export async function sendSpecialKey(
* [DR3-001] Named NAVIGATION_KEY_VALUES to avoid collision with existing SPECIAL_KEY_VALUES.
* [DR2-004] Exported as as const array + type guard (not Set) for immutability guarantee.
*/
export const NAVIGATION_KEY_VALUES = ['Up', 'Down', 'Enter', 'Escape', 'Tab', 'BTab'] as const;
export const NAVIGATION_KEY_VALUES = ['Up', 'Down', 'Left', 'Right', 'Enter', 'Escape', 'Tab', 'BTab'] as const;

/**
* Navigation key type derived from NAVIGATION_KEY_VALUES.
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/special-keys-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ vi.mock('@/lib/db', () => ({
vi.mock('@/lib/tmux/tmux', () => ({
hasSession: vi.fn(),
sendSpecialKeys: vi.fn(),
isAllowedSpecialKey: vi.fn((key: string) => ['Up', 'Down', 'Enter', 'Escape', 'Tab', 'BTab'].includes(key)),
isAllowedSpecialKey: vi.fn((key: string) => ['Up', 'Down', 'Left', 'Right', 'Enter', 'Escape', 'Tab', 'BTab'].includes(key)),
sendSpecialKeysAndInvalidate: vi.fn(),
}));

Expand Down
14 changes: 10 additions & 4 deletions tests/unit/tmux-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe('NAVIGATION_KEY_VALUES', () => {
expect(Array.isArray(NAVIGATION_KEY_VALUES)).toBe(true);
});

it('should contain exactly the 6 allowed navigation keys', () => {
expect(NAVIGATION_KEY_VALUES).toEqual(['Up', 'Down', 'Enter', 'Escape', 'Tab', 'BTab']);
it('should contain exactly the 8 allowed navigation keys', () => {
expect(NAVIGATION_KEY_VALUES).toEqual(['Up', 'Down', 'Left', 'Right', 'Enter', 'Escape', 'Tab', 'BTab']);
});

it('should be distinct from SPECIAL_KEY_VALUES (no name collision)', () => {
Expand All @@ -58,8 +58,8 @@ describe('isAllowedSpecialKey', () => {
expect(isAllowedSpecialKey('C-c')).toBe(false);
expect(isAllowedSpecialKey('C-d')).toBe(false);
expect(isAllowedSpecialKey('C-m')).toBe(false);
expect(isAllowedSpecialKey('Left')).toBe(false);
expect(isAllowedSpecialKey('Right')).toBe(false);
expect(isAllowedSpecialKey('Left')).toBe(true);
expect(isAllowedSpecialKey('Right')).toBe(true);
expect(isAllowedSpecialKey('Space')).toBe(false);
expect(isAllowedSpecialKey('')).toBe(false);
expect(isAllowedSpecialKey('arbitrary-key')).toBe(false);
Expand All @@ -73,6 +73,12 @@ describe('isAllowedSpecialKey', () => {
expect(isAllowedSpecialKey('enter')).toBe(false);
});

it('should reject Space, BSpace, and DC keys (security regression)', () => {
expect(isAllowedSpecialKey('Space')).toBe(false);
expect(isAllowedSpecialKey('BSpace')).toBe(false);
expect(isAllowedSpecialKey('DC')).toBe(false);
});

it('should act as type guard (narrows to NavigationKey)', () => {
const key: string = 'Up';
if (isAllowedSpecialKey(key)) {
Expand Down
Loading