@akiojin/gwt 3.1.2 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.ja.md +3 -4
  2. package/README.md +3 -4
  3. package/dist/claude.d.ts +21 -0
  4. package/dist/claude.d.ts.map +1 -1
  5. package/dist/claude.js +73 -30
  6. package/dist/claude.js.map +1 -1
  7. package/dist/cli/ui/components/common/Select.d.ts +6 -0
  8. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  9. package/dist/cli/ui/components/common/Select.js +3 -2
  10. package/dist/cli/ui/components/common/Select.js.map +1 -1
  11. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +6 -0
  12. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
  13. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +3 -2
  14. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
  15. package/dist/cli/ui/components/screens/BatchMergeProgressScreen.d.ts +3 -0
  16. package/dist/cli/ui/components/screens/BatchMergeProgressScreen.d.ts.map +1 -1
  17. package/dist/cli/ui/components/screens/BatchMergeProgressScreen.js +3 -2
  18. package/dist/cli/ui/components/screens/BatchMergeProgressScreen.js.map +1 -1
  19. package/dist/cli/ui/components/screens/BatchMergeResultScreen.d.ts +3 -0
  20. package/dist/cli/ui/components/screens/BatchMergeResultScreen.d.ts.map +1 -1
  21. package/dist/cli/ui/components/screens/BatchMergeResultScreen.js +3 -2
  22. package/dist/cli/ui/components/screens/BatchMergeResultScreen.js.map +1 -1
  23. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts +3 -0
  24. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts.map +1 -1
  25. package/dist/cli/ui/components/screens/BranchCreatorScreen.js +3 -2
  26. package/dist/cli/ui/components/screens/BranchCreatorScreen.js.map +1 -1
  27. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -0
  28. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  29. package/dist/cli/ui/components/screens/BranchListScreen.js +3 -4
  30. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  31. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts +10 -1
  32. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -1
  33. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +15 -15
  34. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -1
  35. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts +3 -0
  36. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts.map +1 -1
  37. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js +3 -2
  38. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js.map +1 -1
  39. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +15 -0
  40. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +3 -2
  42. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  43. package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts +6 -0
  44. package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -1
  45. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +3 -7
  46. package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -1
  47. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts +6 -0
  48. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts.map +1 -1
  49. package/dist/cli/ui/components/screens/PRCleanupScreen.js +3 -2
  50. package/dist/cli/ui/components/screens/PRCleanupScreen.js.map +1 -1
  51. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +6 -0
  52. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts.map +1 -1
  53. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +3 -2
  54. package/dist/cli/ui/components/screens/SessionSelectorScreen.js.map +1 -1
  55. package/dist/cli/ui/hooks/useAppInput.d.ts +20 -0
  56. package/dist/cli/ui/hooks/useAppInput.d.ts.map +1 -0
  57. package/dist/cli/ui/hooks/useAppInput.js +137 -0
  58. package/dist/cli/ui/hooks/useAppInput.js.map +1 -0
  59. package/dist/cli/ui/screens/BranchActionSelectorScreen.d.ts +3 -0
  60. package/dist/cli/ui/screens/BranchActionSelectorScreen.d.ts.map +1 -1
  61. package/dist/cli/ui/screens/BranchActionSelectorScreen.js +3 -2
  62. package/dist/cli/ui/screens/BranchActionSelectorScreen.js.map +1 -1
  63. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  64. package/dist/cli/ui/utils/branchFormatter.js +0 -2
  65. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  66. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  67. package/dist/cli/ui/utils/modelOptions.js +25 -16
  68. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  69. package/dist/client/assets/{index-f5D2XwDh.js → index-v8smkNOL.js} +16 -16
  70. package/dist/client/index.html +1 -1
  71. package/dist/codex.d.ts +32 -0
  72. package/dist/codex.d.ts.map +1 -1
  73. package/dist/codex.js +32 -1
  74. package/dist/codex.js.map +1 -1
  75. package/dist/config/builtin-tools.d.ts +1 -5
  76. package/dist/config/builtin-tools.d.ts.map +1 -1
  77. package/dist/config/builtin-tools.js +1 -18
  78. package/dist/config/builtin-tools.js.map +1 -1
  79. package/dist/gemini.d.ts +17 -0
  80. package/dist/gemini.d.ts.map +1 -1
  81. package/dist/gemini.js +43 -61
  82. package/dist/gemini.js.map +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +0 -20
  85. package/dist/index.js.map +1 -1
  86. package/dist/utils/command.d.ts +10 -0
  87. package/dist/utils/command.d.ts.map +1 -0
  88. package/dist/utils/command.js +25 -0
  89. package/dist/utils/command.js.map +1 -0
  90. package/dist/utils/session/index.d.ts +0 -2
  91. package/dist/utils/session/index.d.ts.map +1 -1
  92. package/dist/utils/session/index.js +0 -3
  93. package/dist/utils/session/index.js.map +1 -1
  94. package/dist/utils/session/parsers/index.d.ts +0 -1
  95. package/dist/utils/session/parsers/index.d.ts.map +1 -1
  96. package/dist/utils/session/parsers/index.js +0 -2
  97. package/dist/utils/session/parsers/index.js.map +1 -1
  98. package/dist/utils/session.d.ts +0 -1
  99. package/dist/utils/session.d.ts.map +1 -1
  100. package/dist/utils/session.js +0 -1
  101. package/dist/utils/session.js.map +1 -1
  102. package/dist/utils/terminal.d.ts +34 -0
  103. package/dist/utils/terminal.d.ts.map +1 -1
  104. package/dist/utils/terminal.js +51 -4
  105. package/dist/utils/terminal.js.map +1 -1
  106. package/dist/web/client/src/components/branch-detail/BranchInfoCards.d.ts.map +1 -1
  107. package/dist/web/client/src/components/branch-detail/BranchInfoCards.js +0 -2
  108. package/dist/web/client/src/components/branch-detail/BranchInfoCards.js.map +1 -1
  109. package/package.json +1 -1
  110. package/src/claude.ts +92 -34
  111. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +3 -1
  112. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +8 -5
  113. package/src/cli/ui/components/common/Select.tsx +9 -2
  114. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +9 -2
  115. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +6 -2
  116. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +6 -2
  117. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +6 -2
  118. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -4
  119. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +36 -27
  120. package/src/cli/ui/components/screens/EnvironmentProfileScreen.tsx +6 -2
  121. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +18 -2
  122. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +9 -10
  123. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +9 -2
  124. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +9 -2
  125. package/src/cli/ui/hooks/useAppInput.ts +171 -0
  126. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +6 -2
  127. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +68 -1
  128. package/src/cli/ui/utils/branchFormatter.ts +0 -1
  129. package/src/cli/ui/utils/modelOptions.test.ts +9 -6
  130. package/src/cli/ui/utils/modelOptions.ts +25 -18
  131. package/src/codex.ts +40 -1
  132. package/src/config/builtin-tools.ts +1 -19
  133. package/src/gemini.ts +47 -68
  134. package/src/index.ts +0 -26
  135. package/src/utils/command.ts +26 -0
  136. package/src/utils/session/index.ts +0 -4
  137. package/src/utils/session/parsers/index.ts +0 -3
  138. package/src/utils/session.ts +0 -1
  139. package/src/utils/terminal.ts +65 -4
  140. package/src/web/client/src/components/branch-detail/BranchInfoCards.tsx +0 -1
  141. package/dist/qwen.d.ts +0 -16
  142. package/dist/qwen.d.ts.map +0 -1
  143. package/dist/qwen.js +0 -202
  144. package/dist/qwen.js.map +0 -1
  145. package/dist/utils/session/parsers/qwen.d.ts +0 -21
  146. package/dist/utils/session/parsers/qwen.d.ts.map +0 -1
  147. package/dist/utils/session/parsers/qwen.js +0 -36
  148. package/dist/utils/session/parsers/qwen.js.map +0 -1
  149. package/src/qwen.ts +0 -273
  150. package/src/utils/session/parsers/qwen.ts +0 -54
@@ -1,8 +1,9 @@
1
1
  import React, { useEffect, useMemo, useState } from "react";
2
- import { Box, Text, useInput } from "ink";
2
+ import { Box, Text } from "ink";
3
3
  import { Header } from "../parts/Header.js";
4
4
  import { Footer } from "../parts/Footer.js";
5
5
  import { Select, type SelectItem } from "../common/Select.js";
6
+ import { useAppInput } from "../../hooks/useAppInput.js";
6
7
  import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
8
  import type { AITool, InferenceLevel, ModelOption } from "../../types.js";
8
9
  import {
@@ -12,6 +13,9 @@ import {
12
13
  getModelOptions,
13
14
  } from "../../utils/modelOptions.js";
14
15
 
16
+ /**
17
+ * Result returned by `ModelSelectorScreen`.
18
+ */
15
19
  export interface ModelSelectionResult {
16
20
  model: string | null;
17
21
  inferenceLevel?: InferenceLevel;
@@ -25,6 +29,9 @@ interface InferenceSelectItem extends SelectItem {
25
29
  hint?: string;
26
30
  }
27
31
 
32
+ /**
33
+ * Props for `ModelSelectorScreen`.
34
+ */
28
35
  export interface ModelSelectorScreenProps {
29
36
  tool: AITool;
30
37
  onBack: () => void;
@@ -37,7 +44,6 @@ const TOOL_LABELS: Record<string, string> = {
37
44
  "claude-code": "Claude Code",
38
45
  "codex-cli": "Codex",
39
46
  "gemini-cli": "Gemini",
40
- "qwen-cli": "Qwen",
41
47
  };
42
48
 
43
49
  const INFERENCE_LABELS: Record<InferenceLevel, string> = {
@@ -159,7 +165,7 @@ export function ModelSelectorScreen({
159
165
  return index >= 0 ? index : 0;
160
166
  }, [initialSelection?.inferenceLevel, inferenceOptions, selectedModel]);
161
167
 
162
- useInput((_input, key) => {
168
+ useAppInput((_input, key) => {
163
169
  if (key.escape) {
164
170
  if (step === "inference") {
165
171
  setStep("model");
@@ -255,13 +261,6 @@ export function ModelSelectorScreen({
255
261
  {modelOptions.length === 0 ? " (no options)" : ""}
256
262
  </Text>
257
263
  </Box>
258
- {tool === "qwen-cli" ? (
259
- <Box marginBottom={1} flexDirection="column">
260
- <Text>Latest Qwen models from Alibaba Cloud ModelStudio:</Text>
261
- <Text>• coder-model (qwen3-coder-plus-2025-09-23)</Text>
262
- <Text>• vision-model (qwen3-vl-plus-2025-09-23)</Text>
263
- </Box>
264
- ) : null}
265
264
 
266
265
  {modelItems.length === 0 ? (
267
266
  <Select
@@ -1,17 +1,24 @@
1
1
  import React from "react";
2
- import { Box, Text, useInput } from "ink";
2
+ import { Box, Text } from "ink";
3
3
  import { Header } from "../parts/Header.js";
4
4
  import { Footer } from "../parts/Footer.js";
5
5
  import { Select } from "../common/Select.js";
6
+ import { useAppInput } from "../../hooks/useAppInput.js";
6
7
  import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
8
  import type { CleanupTarget } from "../../types.js";
8
9
 
10
+ /**
11
+ * Renderable item for the cleanup target list.
12
+ */
9
13
  export interface PRItem {
10
14
  label: string;
11
15
  value: string;
12
16
  target: CleanupTarget;
13
17
  }
14
18
 
19
+ /**
20
+ * Props for `PRCleanupScreen`.
21
+ */
15
22
  export interface PRCleanupScreenProps {
16
23
  targets: CleanupTarget[];
17
24
  loading: boolean;
@@ -41,7 +48,7 @@ export function PRCleanupScreen({
41
48
 
42
49
  // Handle keyboard input
43
50
  // Note: Select component handles Enter and arrow keys
44
- useInput((input, key) => {
51
+ useAppInput((input, key) => {
45
52
  if (key.escape) {
46
53
  onBack();
47
54
  } else if (input === "r") {
@@ -1,16 +1,23 @@
1
1
  import React from "react";
2
- import { Box, Text, useInput } from "ink";
2
+ import { Box, Text } from "ink";
3
3
  import { Header } from "../parts/Header.js";
4
4
  import { Footer } from "../parts/Footer.js";
5
5
  import { Select } from "../common/Select.js";
6
+ import { useAppInput } from "../../hooks/useAppInput.js";
6
7
  import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
8
 
9
+ /**
10
+ * Renderable item for the session selector list.
11
+ */
8
12
  export interface SessionItem {
9
13
  label: string;
10
14
  value: string;
11
15
  secondary?: string;
12
16
  }
13
17
 
18
+ /**
19
+ * Props for `SessionSelectorScreen`.
20
+ */
14
21
  export interface SessionSelectorScreenProps {
15
22
  sessions: {
16
23
  sessionId: string;
@@ -41,7 +48,7 @@ export function SessionSelectorScreen({
41
48
 
42
49
  // Handle keyboard input
43
50
  // Note: Select component handles Enter and arrow keys
44
- useInput((input, key) => {
51
+ useAppInput((input, key) => {
45
52
  if (key.escape) {
46
53
  onBack();
47
54
  }
@@ -0,0 +1,171 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { useInput, type Key } from "ink";
3
+
4
+ const ESCAPE_SEQUENCE_TIMEOUT_MS = 25;
5
+
6
+ type InputHandler = (input: string, key: Key) => void;
7
+ type Options = { isActive?: boolean };
8
+
9
+ type BufferedEvent = { input: string; key: Key };
10
+
11
+ type PendingEscape = {
12
+ escapeEvent: BufferedEvent;
13
+ bufferedEvents: BufferedEvent[];
14
+ timeoutId: ReturnType<typeof setTimeout> | null;
15
+ };
16
+
17
+ const createEmptyKey = (): Key => ({
18
+ upArrow: false,
19
+ downArrow: false,
20
+ leftArrow: false,
21
+ rightArrow: false,
22
+ pageDown: false,
23
+ pageUp: false,
24
+ return: false,
25
+ escape: false,
26
+ ctrl: false,
27
+ shift: false,
28
+ tab: false,
29
+ backspace: false,
30
+ delete: false,
31
+ meta: false,
32
+ });
33
+
34
+ function parseArrowDirection(
35
+ sequence: string,
36
+ ): "up" | "down" | "left" | "right" | null {
37
+ if (!sequence) {
38
+ return null;
39
+ }
40
+
41
+ const first = sequence[0];
42
+ if (first !== "[" && first !== "O") {
43
+ return null;
44
+ }
45
+
46
+ const last = sequence.at(-1);
47
+ if (!last) {
48
+ return null;
49
+ }
50
+
51
+ switch (last.toUpperCase()) {
52
+ case "A":
53
+ return "up";
54
+ case "B":
55
+ return "down";
56
+ case "C":
57
+ return "right";
58
+ case "D":
59
+ return "left";
60
+ default:
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Ink `useInput` wrapper that buffers an initial Escape press to disambiguate
67
+ * between a real Escape key and a split ANSI escape sequence (e.g., arrow keys
68
+ * in WSL2/Windows terminals).
69
+ *
70
+ * When an Escape is received, waits briefly for subsequent characters. If they
71
+ * form an arrow sequence (e.g., `[A`, `[B`, `OA`), emits a synthetic arrow
72
+ * `Key`. Otherwise, forwards the original Escape keypress.
73
+ *
74
+ * @param inputHandler - Callback invoked for each normalized input event
75
+ * @param options - Optional Ink input options (e.g., `{ isActive: false }`)
76
+ */
77
+ export function useAppInput(
78
+ inputHandler: InputHandler,
79
+ options?: Options,
80
+ ): void {
81
+ const handlerRef = useRef(inputHandler);
82
+ const pendingRef = useRef<PendingEscape | null>(null);
83
+
84
+ useEffect(() => {
85
+ handlerRef.current = inputHandler;
86
+ }, [inputHandler]);
87
+
88
+ const clearPending = useCallback(() => {
89
+ const pending = pendingRef.current;
90
+ if (pending?.timeoutId) {
91
+ clearTimeout(pending.timeoutId);
92
+ }
93
+ pendingRef.current = null;
94
+ }, []);
95
+
96
+ const flushPending = useCallback(() => {
97
+ const pending = pendingRef.current;
98
+ if (!pending) {
99
+ return;
100
+ }
101
+
102
+ clearPending();
103
+ handlerRef.current(pending.escapeEvent.input, pending.escapeEvent.key);
104
+ for (const event of pending.bufferedEvents) {
105
+ handlerRef.current(event.input, event.key);
106
+ }
107
+ }, [clearPending]);
108
+
109
+ useEffect(() => {
110
+ if (options?.isActive === false) {
111
+ clearPending();
112
+ }
113
+ }, [options?.isActive, clearPending]);
114
+
115
+ useEffect(() => clearPending, [clearPending]);
116
+
117
+ useInput((input, key) => {
118
+ const pending = pendingRef.current;
119
+
120
+ if (!pending) {
121
+ if (key.escape) {
122
+ const timeoutId = setTimeout(flushPending, ESCAPE_SEQUENCE_TIMEOUT_MS);
123
+ pendingRef.current = {
124
+ escapeEvent: { input, key },
125
+ bufferedEvents: [],
126
+ timeoutId,
127
+ };
128
+ return;
129
+ }
130
+
131
+ handlerRef.current(input, key);
132
+ return;
133
+ }
134
+
135
+ if (pending.timeoutId) {
136
+ clearTimeout(pending.timeoutId);
137
+ }
138
+
139
+ pending.bufferedEvents.push({ input, key });
140
+
141
+ const [firstEvent] = pending.bufferedEvents;
142
+ const firstInput = firstEvent?.input;
143
+
144
+ if (firstInput && firstInput !== "[" && firstInput !== "O") {
145
+ flushPending();
146
+ return;
147
+ }
148
+
149
+ const sequence = pending.bufferedEvents
150
+ .map((event) => event.input)
151
+ .join("");
152
+ const arrow = parseArrowDirection(sequence);
153
+ if (arrow) {
154
+ clearPending();
155
+ const arrowKey = createEmptyKey();
156
+ if (arrow === "up") {
157
+ arrowKey.upArrow = true;
158
+ } else if (arrow === "down") {
159
+ arrowKey.downArrow = true;
160
+ } else if (arrow === "left") {
161
+ arrowKey.leftArrow = true;
162
+ } else if (arrow === "right") {
163
+ arrowKey.rightArrow = true;
164
+ }
165
+ handlerRef.current("", arrowKey);
166
+ return;
167
+ }
168
+
169
+ pending.timeoutId = setTimeout(flushPending, ESCAPE_SEQUENCE_TIMEOUT_MS);
170
+ }, options);
171
+ }
@@ -1,9 +1,13 @@
1
1
  import React from "react";
2
- import { Box, Text, useInput } from "ink";
2
+ import { Box, Text } from "ink";
3
3
  import { Select, type SelectItem } from "../components/common/Select.js";
4
4
  import { Footer } from "../components/parts/Footer.js";
5
+ import { useAppInput } from "../hooks/useAppInput.js";
5
6
  import type { BranchAction } from "../types.js";
6
7
 
8
+ /**
9
+ * Props for `BranchActionSelectorScreen`.
10
+ */
7
11
  export interface BranchActionSelectorScreenProps {
8
12
  selectedBranch: string;
9
13
  onUseExisting: () => void;
@@ -35,7 +39,7 @@ export function BranchActionSelectorScreen({
35
39
  secondaryLabel,
36
40
  }: BranchActionSelectorScreenProps) {
37
41
  // Handle keyboard input for back navigation
38
- useInput((input, key) => {
42
+ useAppInput((input, key) => {
39
43
  if (key.escape) {
40
44
  onBack();
41
45
  }
@@ -2,7 +2,8 @@
2
2
  * @vitest-environment happy-dom
3
3
  */
4
4
  import { describe, it, expect, beforeEach, vi } from "vitest";
5
- import { render } from "@testing-library/react";
5
+ import { act, render } from "@testing-library/react";
6
+ import { render as inkRender } from "ink-testing-library";
6
7
  import React from "react";
7
8
  import { BranchActionSelectorScreen } from "../BranchActionSelectorScreen.js";
8
9
  import { Window } from "happy-dom";
@@ -149,4 +150,70 @@ describe("BranchActionSelectorScreen", () => {
149
150
  // For now, we verify the component structure and callbacks are set up
150
151
  expect(onCreateNew).not.toHaveBeenCalled();
151
152
  });
153
+
154
+ it("should treat split down-arrow sequence as navigation (WSL2) and not as Escape", () => {
155
+ const onUseExisting = vi.fn();
156
+ const onCreateNew = vi.fn();
157
+ const onBack = vi.fn();
158
+
159
+ const inkApp = inkRender(
160
+ <BranchActionSelectorScreen
161
+ selectedBranch="feature-test"
162
+ onUseExisting={onUseExisting}
163
+ onCreateNew={onCreateNew}
164
+ onBack={onBack}
165
+ />,
166
+ );
167
+
168
+ act(() => {
169
+ inkApp.stdin.write("\u001b");
170
+ inkApp.stdin.write("[");
171
+ inkApp.stdin.write("B");
172
+ });
173
+
174
+ act(() => {
175
+ inkApp.stdin.write("\r");
176
+ });
177
+
178
+ expect(onBack).not.toHaveBeenCalled();
179
+ expect(onCreateNew).toHaveBeenCalledTimes(1);
180
+ expect(onUseExisting).not.toHaveBeenCalled();
181
+
182
+ inkApp.unmount();
183
+ });
184
+
185
+ it("should still handle Escape key as back navigation", () => {
186
+ vi.useFakeTimers();
187
+ let inkApp: ReturnType<typeof inkRender> | undefined;
188
+
189
+ try {
190
+ const onUseExisting = vi.fn();
191
+ const onCreateNew = vi.fn();
192
+ const onBack = vi.fn();
193
+
194
+ inkApp = inkRender(
195
+ <BranchActionSelectorScreen
196
+ selectedBranch="feature-test"
197
+ onUseExisting={onUseExisting}
198
+ onCreateNew={onCreateNew}
199
+ onBack={onBack}
200
+ />,
201
+ );
202
+
203
+ act(() => {
204
+ inkApp.stdin.write("\u001b");
205
+ });
206
+
207
+ act(() => {
208
+ vi.advanceTimersByTime(25);
209
+ });
210
+
211
+ expect(onBack).toHaveBeenCalledTimes(1);
212
+ expect(onCreateNew).not.toHaveBeenCalled();
213
+ expect(onUseExisting).not.toHaveBeenCalled();
214
+ } finally {
215
+ inkApp?.unmount();
216
+ vi.useRealTimers();
217
+ }
218
+ });
152
219
  });
@@ -98,7 +98,6 @@ function mapToolLabel(toolId: string, toolLabel?: string): string {
98
98
  if (toolId === "claude-code") return "Claude";
99
99
  if (toolId === "codex-cli") return "Codex";
100
100
  if (toolId === "gemini-cli") return "Gemini";
101
- if (toolId === "qwen-cli") return "Qwen";
102
101
  if (toolLabel) return toolLabel;
103
102
  return "Custom";
104
103
  }
@@ -8,13 +8,13 @@ import {
8
8
  const byId = (tool: string) => getModelOptions(tool).map((m) => m.id);
9
9
 
10
10
  describe("modelOptions", () => {
11
- it("lists Claude official aliases and sets Opus 4.5 as default", () => {
11
+ it("lists Claude official aliases and sets Default as default", () => {
12
12
  const options = getModelOptions("claude-code");
13
13
  const ids = options.map((m) => m.id);
14
- expect(ids).toEqual(["opus", "sonnet", "haiku"]);
14
+ expect(ids).toEqual(["", "opus", "sonnet", "haiku"]);
15
15
  const defaultModel = getDefaultModelOption("claude-code");
16
- expect(defaultModel?.id).toBe("opus");
17
- expect(defaultModel?.label).toBe("Opus 4.5");
16
+ expect(defaultModel?.id).toBe("");
17
+ expect(defaultModel?.label).toBe("Default (Auto)");
18
18
  });
19
19
 
20
20
  it("has unique Codex models", () => {
@@ -22,6 +22,7 @@ describe("modelOptions", () => {
22
22
  const unique = new Set(ids);
23
23
  expect(unique.size).toBe(ids.length);
24
24
  expect(ids).toEqual([
25
+ "",
25
26
  "gpt-5.1-codex",
26
27
  "gpt-5.2",
27
28
  "gpt-5.1-codex-max",
@@ -52,14 +53,16 @@ describe("modelOptions", () => {
52
53
 
53
54
  it("lists expected Gemini models", () => {
54
55
  expect(byId("gemini-cli")).toEqual([
56
+ "",
55
57
  "gemini-3-pro-preview",
58
+ "gemini-3-flash-preview",
56
59
  "gemini-2.5-pro",
57
60
  "gemini-2.5-flash",
58
61
  "gemini-2.5-flash-lite",
59
62
  ]);
60
63
  });
61
64
 
62
- it("lists expected Qwen models", () => {
63
- expect(byId("qwen-cli")).toEqual(["coder-model", "vision-model"]);
65
+ it("returns no models for unsupported tools", () => {
66
+ expect(byId("unknown-tool")).toEqual([]);
64
67
  });
65
68
  });
@@ -5,12 +5,17 @@ const CODEX_MAX_LEVELS: InferenceLevel[] = ["xhigh", "high", "medium", "low"];
5
5
 
6
6
  const MODEL_OPTIONS: Record<string, ModelOption[]> = {
7
7
  "claude-code": [
8
+ {
9
+ id: "",
10
+ label: "Default (Auto)",
11
+ description: "Use Claude Code default behavior",
12
+ isDefault: true,
13
+ },
8
14
  {
9
15
  id: "opus",
10
16
  label: "Opus 4.5",
11
17
  description:
12
18
  "Official Opus alias for Claude Code (non-custom, matches /model option).",
13
- isDefault: true,
14
19
  },
15
20
  {
16
21
  id: "sonnet",
@@ -25,13 +30,20 @@ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
25
30
  },
26
31
  ],
27
32
  "codex-cli": [
33
+ {
34
+ id: "",
35
+ label: "Default (Auto)",
36
+ description: "Use Codex default model",
37
+ isDefault: true,
38
+ inferenceLevels: CODEX_BASE_LEVELS,
39
+ defaultInference: "high",
40
+ },
28
41
  {
29
42
  id: "gpt-5.1-codex",
30
43
  label: "gpt-5.1-codex",
31
44
  description: "Standard Codex model",
32
45
  inferenceLevels: CODEX_BASE_LEVELS,
33
46
  defaultInference: "high",
34
- isDefault: true,
35
47
  },
36
48
  {
37
49
  id: "gpt-5.2",
@@ -63,12 +75,22 @@ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
63
75
  },
64
76
  ],
65
77
  "gemini-cli": [
78
+ {
79
+ id: "",
80
+ label: "Default (Auto)",
81
+ description: "Use Gemini CLI default model",
82
+ isDefault: true,
83
+ },
66
84
  {
67
85
  id: "gemini-3-pro-preview",
68
86
  label: "Pro (gemini-3-pro-preview)",
69
87
  description:
70
88
  "Default Pro. Falls back to gemini-2.5-pro when preview is unavailable.",
71
- isDefault: true,
89
+ },
90
+ {
91
+ id: "gemini-3-flash-preview",
92
+ label: "Flash (gemini-3-flash-preview)",
93
+ description: "Next-generation high-speed model",
72
94
  },
73
95
  {
74
96
  id: "gemini-2.5-pro",
@@ -86,21 +108,6 @@ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
86
108
  description: "Fastest for simple tasks",
87
109
  },
88
110
  ],
89
- "qwen-cli": [
90
- {
91
- id: "coder-model",
92
- label: "Coder Model",
93
- description:
94
- "Latest Qwen Coder model (qwen3-coder-plus-2025-09-23) from Alibaba Cloud ModelStudio",
95
- isDefault: true,
96
- },
97
- {
98
- id: "vision-model",
99
- label: "Vision Model",
100
- description:
101
- "Latest Qwen Vision model (qwen3-vl-plus-2025-09-23) from Alibaba Cloud ModelStudio",
102
- },
103
- ],
104
111
  };
105
112
 
106
113
  export function getModelOptions(tool: AITool): ModelOption[] {
package/src/codex.ts CHANGED
@@ -2,16 +2,36 @@ import { execa } from "execa";
2
2
  import chalk from "chalk";
3
3
  import { platform } from "os";
4
4
  import { existsSync } from "fs";
5
- import { createChildStdio, getTerminalStreams } from "./utils/terminal.js";
5
+ import {
6
+ createChildStdio,
7
+ getTerminalStreams,
8
+ resetTerminalModes,
9
+ } from "./utils/terminal.js";
6
10
  import { findLatestCodexSession } from "./utils/session.js";
7
11
 
8
12
  const CODEX_CLI_PACKAGE = "@openai/codex@latest";
9
13
 
14
+ /**
15
+ * Reasoning effort levels supported by Codex CLI.
16
+ */
10
17
  export type CodexReasoningEffort = "low" | "medium" | "high" | "xhigh";
11
18
 
19
+ /**
20
+ * Default Codex model used when no override is provided.
21
+ */
12
22
  export const DEFAULT_CODEX_MODEL = "gpt-5.1-codex";
23
+
24
+ /**
25
+ * Default reasoning effort used when no override is provided.
26
+ */
13
27
  export const DEFAULT_CODEX_REASONING_EFFORT: CodexReasoningEffort = "high";
14
28
 
29
+ /**
30
+ * Builds the default argument list for Codex CLI launch.
31
+ *
32
+ * @param model - Model name to pass via `--model`
33
+ * @param reasoningEffort - Reasoning effort to pass via config
34
+ */
15
35
  export const buildDefaultCodexArgs = (
16
36
  model: string = DEFAULT_CODEX_MODEL,
17
37
  reasoningEffort: CodexReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT,
@@ -37,6 +57,10 @@ export const buildDefaultCodexArgs = (
37
57
  "shell_environment_policy.experimental_use_profile=true",
38
58
  ];
39
59
 
60
+ /**
61
+ * Error wrapper used by `launchCodexCLI` to preserve the original failure
62
+ * while providing a user-friendly message.
63
+ */
40
64
  export class CodexError extends Error {
41
65
  constructor(
42
66
  message: string,
@@ -47,6 +71,16 @@ export class CodexError extends Error {
47
71
  }
48
72
  }
49
73
 
74
+ /**
75
+ * Launches Codex CLI in the given worktree path.
76
+ *
77
+ * This function resets terminal modes before and after the child process and
78
+ * tries to detect a session id after launch (when supported).
79
+ *
80
+ * @param worktreePath - Worktree directory to run Codex CLI in
81
+ * @param options - Launch options (mode/session/model/reasoning/env)
82
+ * @returns Captured session id when available
83
+ */
50
84
  export async function launchCodexCLI(
51
85
  worktreePath: string,
52
86
  options: {
@@ -133,6 +167,7 @@ export async function launchCodexCLI(
133
167
  console.log(chalk.gray(` 📋 Args: ${args.join(" ")}`));
134
168
 
135
169
  terminal.exitRawMode();
170
+ resetTerminalModes(terminal.stdout);
136
171
 
137
172
  const childStdio = createChildStdio();
138
173
 
@@ -235,9 +270,13 @@ export async function launchCodexCLI(
235
270
  throw new CodexError(errorMessage, error);
236
271
  } finally {
237
272
  terminal.exitRawMode();
273
+ resetTerminalModes(terminal.stdout);
238
274
  }
239
275
  }
240
276
 
277
+ /**
278
+ * Checks whether Codex CLI is available via `bunx` in the current environment.
279
+ */
241
280
  export async function isCodexAvailable(): Promise<boolean> {
242
281
  try {
243
282
  await execa("bunx", [CODEX_CLI_PACKAGE, "--help"]);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ビルトインAIツール定義
3
3
  *
4
- * Claude Code、Codex、Gemini、Qwen の CustomAITool 形式定義
4
+ * Claude Code、Codex、Gemini の CustomAITool 形式定義
5
5
  */
6
6
 
7
7
  import type { CustomAITool } from "../types/tools.js";
@@ -58,23 +58,6 @@ export const GEMINI_CLI_TOOL: CustomAITool = {
58
58
  permissionSkipArgs: ["-y"],
59
59
  };
60
60
 
61
- /**
62
- * Qwen のビルトイン定義
63
- */
64
- export const QWEN_CLI_TOOL: CustomAITool = {
65
- id: "qwen-cli",
66
- displayName: "Qwen",
67
- type: "bunx",
68
- command: "@qwen-code/qwen-code@latest",
69
- defaultArgs: ["--checkpointing"],
70
- modeArgs: {
71
- normal: [],
72
- continue: [],
73
- resume: [],
74
- },
75
- permissionSkipArgs: ["--yolo"],
76
- };
77
-
78
61
  /**
79
62
  * すべてのビルトインツール
80
63
  */
@@ -82,5 +65,4 @@ export const BUILTIN_TOOLS: CustomAITool[] = [
82
65
  CLAUDE_CODE_TOOL,
83
66
  CODEX_CLI_TOOL,
84
67
  GEMINI_CLI_TOOL,
85
- QWEN_CLI_TOOL,
86
68
  ];