@akiojin/gwt 2.2.0 → 2.4.0

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 (172) hide show
  1. package/README.ja.md +6 -4
  2. package/README.md +6 -4
  3. package/dist/claude.d.ts +1 -0
  4. package/dist/claude.d.ts.map +1 -1
  5. package/dist/claude.js +6 -3
  6. package/dist/claude.js.map +1 -1
  7. package/dist/cli/ui/components/App.d.ts +6 -4
  8. package/dist/cli/ui/components/App.d.ts.map +1 -1
  9. package/dist/cli/ui/components/App.js +184 -107
  10. package/dist/cli/ui/components/App.js.map +1 -1
  11. package/dist/cli/ui/components/common/Confirm.d.ts +1 -1
  12. package/dist/cli/ui/components/common/Confirm.d.ts.map +1 -1
  13. package/dist/cli/ui/components/common/Confirm.js +7 -7
  14. package/dist/cli/ui/components/common/Confirm.js.map +1 -1
  15. package/dist/cli/ui/components/common/ErrorBoundary.d.ts +1 -1
  16. package/dist/cli/ui/components/common/ErrorBoundary.d.ts.map +1 -1
  17. package/dist/cli/ui/components/common/ErrorBoundary.js +4 -4
  18. package/dist/cli/ui/components/common/ErrorBoundary.js.map +1 -1
  19. package/dist/cli/ui/components/common/Input.d.ts +2 -2
  20. package/dist/cli/ui/components/common/Input.d.ts.map +1 -1
  21. package/dist/cli/ui/components/common/Input.js +4 -4
  22. package/dist/cli/ui/components/common/Input.js.map +1 -1
  23. package/dist/cli/ui/components/common/LoadingIndicator.d.ts +1 -1
  24. package/dist/cli/ui/components/common/LoadingIndicator.d.ts.map +1 -1
  25. package/dist/cli/ui/components/common/LoadingIndicator.js +4 -4
  26. package/dist/cli/ui/components/common/LoadingIndicator.js.map +1 -1
  27. package/dist/cli/ui/components/common/Select.d.ts +1 -1
  28. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  29. package/dist/cli/ui/components/common/Select.js +11 -12
  30. package/dist/cli/ui/components/common/Select.js.map +1 -1
  31. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +3 -3
  32. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
  33. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +11 -11
  34. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
  35. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts +1 -1
  36. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts.map +1 -1
  37. package/dist/cli/ui/components/screens/BranchCreatorScreen.js +39 -36
  38. package/dist/cli/ui/components/screens/BranchCreatorScreen.js.map +1 -1
  39. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -3
  40. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/components/screens/BranchListScreen.js +55 -50
  42. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  43. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -2
  44. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  45. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +25 -25
  46. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  47. package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts +18 -0
  48. package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -0
  49. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +201 -0
  50. package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -0
  51. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts +2 -2
  52. package/dist/cli/ui/components/screens/PRCleanupScreen.js +21 -21
  53. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +1 -1
  54. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +8 -8
  55. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +1 -1
  56. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +8 -8
  57. package/dist/cli/ui/screens/BranchActionSelectorScreen.d.ts.map +1 -1
  58. package/dist/cli/ui/screens/BranchActionSelectorScreen.js +7 -4
  59. package/dist/cli/ui/screens/BranchActionSelectorScreen.js.map +1 -1
  60. package/dist/cli/ui/types.d.ts +11 -1
  61. package/dist/cli/ui/types.d.ts.map +1 -1
  62. package/dist/cli/ui/utils/modelOptions.d.ts +6 -0
  63. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -0
  64. package/dist/cli/ui/utils/modelOptions.js +111 -0
  65. package/dist/cli/ui/utils/modelOptions.js.map +1 -0
  66. package/dist/client/assets/{index-V6hDu9KS.js → index-Difv1Hwu.js} +2 -2
  67. package/dist/client/index.html +1 -1
  68. package/dist/codex.d.ts +6 -0
  69. package/dist/codex.d.ts.map +1 -1
  70. package/dist/codex.js +11 -4
  71. package/dist/codex.js.map +1 -1
  72. package/dist/config/builtin-tools.d.ts +10 -2
  73. package/dist/config/builtin-tools.d.ts.map +1 -1
  74. package/dist/config/builtin-tools.js +40 -4
  75. package/dist/config/builtin-tools.js.map +1 -1
  76. package/dist/config/index.d.ts.map +1 -1
  77. package/dist/config/index.js.map +1 -1
  78. package/dist/config/tools.d.ts.map +1 -1
  79. package/dist/config/tools.js +4 -3
  80. package/dist/config/tools.js.map +1 -1
  81. package/dist/gemini.d.ts +13 -0
  82. package/dist/gemini.d.ts.map +1 -0
  83. package/dist/gemini.js +157 -0
  84. package/dist/gemini.js.map +1 -0
  85. package/dist/git.d.ts.map +1 -1
  86. package/dist/git.js.map +1 -1
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +59 -7
  89. package/dist/index.js.map +1 -1
  90. package/dist/qwen.d.ts +13 -0
  91. package/dist/qwen.d.ts.map +1 -0
  92. package/dist/qwen.js +157 -0
  93. package/dist/qwen.js.map +1 -0
  94. package/dist/services/git.service.d.ts.map +1 -1
  95. package/dist/services/git.service.js.map +1 -1
  96. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  97. package/dist/web/client/src/components/BranchGraph.js +1 -1
  98. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  99. package/dist/web/client/src/components/EnvEditor.d.ts.map +1 -1
  100. package/dist/web/client/src/components/EnvEditor.js +7 -4
  101. package/dist/web/client/src/components/EnvEditor.js.map +1 -1
  102. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  103. package/dist/web/client/src/pages/BranchDetailPage.js +55 -18
  104. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  105. package/dist/web/client/src/pages/BranchListPage.d.ts.map +1 -1
  106. package/dist/web/client/src/pages/BranchListPage.js +10 -4
  107. package/dist/web/client/src/pages/BranchListPage.js.map +1 -1
  108. package/dist/web/client/src/pages/ConfigManagementPage.d.ts.map +1 -1
  109. package/dist/web/client/src/pages/ConfigManagementPage.js +4 -2
  110. package/dist/web/client/src/pages/ConfigManagementPage.js.map +1 -1
  111. package/package.json +2 -1
  112. package/src/claude.ts +8 -3
  113. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +69 -50
  114. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +67 -45
  115. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +117 -75
  116. package/src/cli/ui/__tests__/components/App.test.tsx +45 -37
  117. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +81 -0
  118. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +35 -22
  119. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +22 -22
  120. package/src/cli/ui/__tests__/components/common/Input.test.tsx +29 -22
  121. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +63 -43
  122. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +57 -66
  123. package/src/cli/ui/__tests__/components/common/Select.test.tsx +121 -91
  124. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +18 -16
  125. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +13 -13
  126. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +20 -20
  127. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +38 -26
  128. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +31 -31
  129. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +73 -37
  130. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +261 -153
  131. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +38 -32
  132. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +39 -39
  133. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +49 -21
  134. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +52 -28
  135. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -48
  136. package/src/cli/ui/__tests__/integration/navigation.test.tsx +111 -83
  137. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +111 -108
  138. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +50 -37
  139. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +75 -76
  140. package/src/cli/ui/components/App.tsx +317 -150
  141. package/src/cli/ui/components/common/Confirm.tsx +13 -9
  142. package/src/cli/ui/components/common/ErrorBoundary.tsx +8 -5
  143. package/src/cli/ui/components/common/Input.tsx +12 -4
  144. package/src/cli/ui/components/common/LoadingIndicator.tsx +8 -5
  145. package/src/cli/ui/components/common/Select.tsx +28 -17
  146. package/src/cli/ui/components/parts/Header.test.tsx +5 -15
  147. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +20 -15
  148. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +74 -54
  149. package/src/cli/ui/components/screens/BranchListScreen.tsx +92 -75
  150. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +35 -28
  151. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +320 -0
  152. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +22 -22
  153. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +8 -8
  154. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +8 -8
  155. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +9 -4
  156. package/src/cli/ui/types.ts +21 -1
  157. package/src/cli/ui/utils/modelOptions.test.ts +36 -0
  158. package/src/cli/ui/utils/modelOptions.ts +122 -0
  159. package/src/codex.ts +23 -4
  160. package/src/config/builtin-tools.ts +42 -4
  161. package/src/config/index.ts +2 -12
  162. package/src/config/tools.ts +16 -6
  163. package/src/gemini.ts +207 -0
  164. package/src/git.ts +2 -1
  165. package/src/index.ts +86 -6
  166. package/src/qwen.ts +213 -0
  167. package/src/services/git.service.ts +2 -1
  168. package/src/web/client/src/components/BranchGraph.tsx +3 -2
  169. package/src/web/client/src/components/EnvEditor.tsx +44 -11
  170. package/src/web/client/src/pages/BranchDetailPage.tsx +165 -54
  171. package/src/web/client/src/pages/BranchListPage.tsx +37 -13
  172. package/src/web/client/src/pages/ConfigManagementPage.tsx +28 -9
@@ -0,0 +1,320 @@
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select, type SelectItem } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
+ import type { AITool, InferenceLevel, ModelOption } from "../../types.js";
8
+ import {
9
+ getDefaultInferenceForModel,
10
+ getDefaultModelOption,
11
+ getInferenceLevelsForModel,
12
+ getModelOptions,
13
+ } from "../../utils/modelOptions.js";
14
+
15
+ export interface ModelSelectionResult {
16
+ model: string | null;
17
+ inferenceLevel?: InferenceLevel;
18
+ }
19
+
20
+ interface ModelSelectItem extends SelectItem {
21
+ description?: string;
22
+ }
23
+
24
+ interface InferenceSelectItem extends SelectItem {
25
+ hint?: string;
26
+ }
27
+
28
+ export interface ModelSelectorScreenProps {
29
+ tool: AITool;
30
+ onBack: () => void;
31
+ onSelect: (selection: ModelSelectionResult) => void;
32
+ version?: string | null;
33
+ initialSelection?: ModelSelectionResult | null;
34
+ }
35
+
36
+ const TOOL_LABELS: Record<string, string> = {
37
+ "claude-code": "Claude Code",
38
+ "codex-cli": "Codex",
39
+ "gemini-cli": "Gemini",
40
+ "qwen-cli": "Qwen",
41
+ };
42
+
43
+ const INFERENCE_LABELS: Record<InferenceLevel, string> = {
44
+ low: "Low (lighter reasoning)",
45
+ medium: "Medium (balanced reasoning)",
46
+ high: "High (deeper reasoning)",
47
+ xhigh: "Extra high (maximum reasoning)",
48
+ };
49
+
50
+ /**
51
+ * モデル選択 → (必要なら) 推論レベル選択を行う画面
52
+ */
53
+ export function ModelSelectorScreen({
54
+ tool,
55
+ onBack,
56
+ onSelect,
57
+ version,
58
+ initialSelection,
59
+ }: ModelSelectorScreenProps) {
60
+ const { rows } = useTerminalSize();
61
+
62
+ const [step, setStep] = useState<"model" | "inference">("model");
63
+ const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
64
+ const [selectedModel, setSelectedModel] = useState<ModelOption | null>(null);
65
+
66
+ // モデル候補をツールに応じてロード
67
+ useEffect(() => {
68
+ const options = getModelOptions(tool);
69
+ setModelOptions(options);
70
+ // 初期選択が有効なら保持
71
+ if (initialSelection?.model) {
72
+ const found = options.find((opt) => opt.id === initialSelection.model);
73
+ if (found) {
74
+ setSelectedModel(found);
75
+ setStep("model");
76
+ return;
77
+ }
78
+ }
79
+ setSelectedModel(null);
80
+ setStep("model");
81
+ }, [tool, initialSelection?.model]);
82
+
83
+ const modelItems: ModelSelectItem[] = useMemo(
84
+ () =>
85
+ modelOptions.map((option) => ({
86
+ label: option.label,
87
+ value: option.id,
88
+ ...(option.description ? { description: option.description } : {}),
89
+ })),
90
+ [modelOptions],
91
+ );
92
+
93
+ const defaultModelIndex = useMemo(() => {
94
+ const initial = initialSelection?.model
95
+ ? modelOptions.findIndex((opt) => opt.id === initialSelection.model)
96
+ : -1;
97
+ if (initial !== -1) return initial;
98
+ const defaultOption = getDefaultModelOption(tool);
99
+ if (!defaultOption) return 0;
100
+ const index = modelOptions.findIndex((opt) => opt.id === defaultOption.id);
101
+ return index >= 0 ? index : 0;
102
+ }, [initialSelection?.model, modelOptions, tool]);
103
+
104
+ const inferenceOptions = useMemo(
105
+ () => getInferenceLevelsForModel(selectedModel ?? undefined),
106
+ [selectedModel],
107
+ );
108
+
109
+ const inferenceItems: InferenceSelectItem[] = useMemo(
110
+ () => {
111
+ return inferenceOptions.map((level) => {
112
+ if (selectedModel?.id === "gpt-5.1-codex-max") {
113
+ if (level === "low") {
114
+ return {
115
+ label: "Low",
116
+ value: level,
117
+ hint: "Fast responses with lighter reasoning",
118
+ };
119
+ }
120
+ if (level === "medium") {
121
+ return {
122
+ label: "Medium (default)",
123
+ value: level,
124
+ hint: "Balances speed and reasoning depth for everyday tasks",
125
+ };
126
+ }
127
+ if (level === "high") {
128
+ return {
129
+ label: "High",
130
+ value: level,
131
+ hint: "Maximizes reasoning depth for complex problems",
132
+ };
133
+ }
134
+ if (level === "xhigh") {
135
+ return {
136
+ label: "Extra high",
137
+ value: level,
138
+ hint:
139
+ "Extra high reasoning depth; may quickly consume Plus plan rate limits.",
140
+ };
141
+ }
142
+ }
143
+
144
+ return {
145
+ label: INFERENCE_LABELS[level],
146
+ value: level,
147
+ };
148
+ });
149
+ },
150
+ [inferenceOptions, selectedModel?.id],
151
+ );
152
+
153
+ const defaultInferenceIndex = useMemo(() => {
154
+ const initialLevel = initialSelection?.inferenceLevel;
155
+ if (initialLevel && inferenceOptions.includes(initialLevel)) {
156
+ return inferenceOptions.findIndex((lvl) => lvl === initialLevel);
157
+ }
158
+ const defaultLevel = getDefaultInferenceForModel(selectedModel ?? undefined);
159
+ if (!defaultLevel) return 0;
160
+ const index = inferenceOptions.findIndex((lvl) => lvl === defaultLevel);
161
+ return index >= 0 ? index : 0;
162
+ }, [initialSelection?.inferenceLevel, inferenceOptions, selectedModel]);
163
+
164
+ useInput((_input, key) => {
165
+ if (key.escape) {
166
+ if (step === "inference") {
167
+ setStep("model");
168
+ return;
169
+ }
170
+ onBack();
171
+ }
172
+ });
173
+
174
+ const handleModelSelect = (item: ModelSelectItem) => {
175
+ const option =
176
+ modelOptions.find((opt) => opt.id === item.value) ?? modelOptions[0];
177
+
178
+ if (!option) {
179
+ onSelect({ model: null });
180
+ return;
181
+ }
182
+
183
+ setSelectedModel(option);
184
+
185
+ const levels = getInferenceLevelsForModel(option);
186
+ if (levels.length > 0) {
187
+ setStep("inference");
188
+ } else {
189
+ onSelect({ model: option.id });
190
+ }
191
+ };
192
+
193
+ const handleInferenceSelect = (item: InferenceSelectItem) => {
194
+ if (!selectedModel) {
195
+ setStep("model");
196
+ return;
197
+ }
198
+
199
+ onSelect({
200
+ model: selectedModel.id,
201
+ inferenceLevel: item.value as InferenceLevel,
202
+ });
203
+ };
204
+
205
+ const footerActions =
206
+ step === "model"
207
+ ? [
208
+ { key: "enter", description: "Select" },
209
+ { key: "esc", description: "Back" },
210
+ ]
211
+ : [
212
+ { key: "enter", description: "Select" },
213
+ { key: "esc", description: "Back to model" },
214
+ ];
215
+
216
+ const toolLabel = TOOL_LABELS[tool] ?? tool;
217
+
218
+ const renderModelItem = (
219
+ item: ModelSelectItem,
220
+ isSelected: boolean,
221
+ ): React.ReactNode => (
222
+ <Box flexDirection="column">
223
+ {isSelected ? (
224
+ <Text color="cyan">➤ {item.label}</Text>
225
+ ) : (
226
+ <Text> {item.label}</Text>
227
+ )}
228
+ {item.description ? (
229
+ <Text color="gray"> {item.description}</Text>
230
+ ) : null}
231
+ </Box>
232
+ );
233
+
234
+ return (
235
+ <Box flexDirection="column" height={rows}>
236
+ <Header
237
+ title={step === "model" ? "Model Selection" : "Inference Level"}
238
+ titleColor="blue"
239
+ version={version}
240
+ />
241
+
242
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
243
+ {step === "model" ? (
244
+ <>
245
+ {tool === "gemini-cli" ? (
246
+ <Box marginBottom={1} flexDirection="column">
247
+ <Text>Gemini 3 preview is enabled.</Text>
248
+ <Text>
249
+ Selecting Pro uses gemini-3-pro-preview and falls back to
250
+ gemini-2.5-pro if unavailable.
251
+ </Text>
252
+ <Text>Use --model to pin a specific Gemini model.</Text>
253
+ </Box>
254
+ ) : null}
255
+
256
+ <Box marginBottom={1}>
257
+ <Text>
258
+ Select a model for {toolLabel}
259
+ {modelOptions.length === 0 ? " (no options)" : ""}
260
+ </Text>
261
+ </Box>
262
+ {tool === "qwen-cli" ? (
263
+ <Box marginBottom={1} flexDirection="column">
264
+ <Text>Latest Qwen models from Alibaba Cloud ModelStudio:</Text>
265
+ <Text>• coder-model (qwen3-coder-plus-2025-09-23)</Text>
266
+ <Text>• vision-model (qwen3-vl-plus-2025-09-23)</Text>
267
+ </Box>
268
+ ) : null}
269
+
270
+ {modelItems.length === 0 ? (
271
+ <Select
272
+ items={[
273
+ {
274
+ label: "No model selection required. Press Enter to continue.",
275
+ value: "__continue__",
276
+ },
277
+ ]}
278
+ onSelect={() => onSelect({ model: null })}
279
+ />
280
+ ) : (
281
+ <Select
282
+ items={modelItems}
283
+ onSelect={handleModelSelect}
284
+ initialIndex={defaultModelIndex}
285
+ renderItem={renderModelItem}
286
+ />
287
+ )}
288
+ </>
289
+ ) : (
290
+ <>
291
+ <Box marginBottom={1}>
292
+ <Text>
293
+ Select reasoning level for {selectedModel?.label ?? "model"}
294
+ </Text>
295
+ </Box>
296
+ <Select
297
+ items={inferenceItems}
298
+ onSelect={handleInferenceSelect}
299
+ initialIndex={defaultInferenceIndex}
300
+ renderItem={(item, isSelected) => (
301
+ <Box flexDirection="column">
302
+ {isSelected ? (
303
+ <Text color="cyan">➤ {item.label}</Text>
304
+ ) : (
305
+ <Text> {item.label}</Text>
306
+ )}
307
+ {"hint" in item && item.hint ? (
308
+ <Text color="gray"> {item.hint}</Text>
309
+ ) : null}
310
+ </Box>
311
+ )}
312
+ />
313
+ </>
314
+ )}
315
+ </Box>
316
+
317
+ <Footer actions={footerActions} />
318
+ </Box>
319
+ );
320
+ }
@@ -1,10 +1,10 @@
1
- import React from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
7
- import type { CleanupTarget } from '../../types.js';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
+ import type { CleanupTarget } from "../../types.js";
8
8
 
9
9
  export interface PRItem {
10
10
  label: string;
@@ -44,7 +44,7 @@ export function PRCleanupScreen({
44
44
  useInput((input, key) => {
45
45
  if (key.escape) {
46
46
  onBack();
47
- } else if (input === 'r') {
47
+ } else if (input === "r") {
48
48
  onRefresh();
49
49
  }
50
50
  });
@@ -53,28 +53,28 @@ export function PRCleanupScreen({
53
53
  const prItems: PRItem[] = targets.map((target) => {
54
54
  const pr = target.pullRequest;
55
55
  const flags: string[] = [];
56
- if (target.cleanupType === 'worktree-and-branch') {
57
- flags.push('worktree');
56
+ if (target.cleanupType === "worktree-and-branch") {
57
+ flags.push("worktree");
58
58
  } else {
59
- flags.push('branch');
59
+ flags.push("branch");
60
60
  }
61
- if (target.reasons?.includes('merged-pr')) {
62
- flags.push('merged');
61
+ if (target.reasons?.includes("merged-pr")) {
62
+ flags.push("merged");
63
63
  }
64
- if (target.reasons?.includes('no-diff-with-base')) {
65
- flags.push('base');
64
+ if (target.reasons?.includes("no-diff-with-base")) {
65
+ flags.push("base");
66
66
  }
67
67
  if (target.hasUncommittedChanges) {
68
- flags.push('changes');
68
+ flags.push("changes");
69
69
  }
70
70
  if (target.hasUnpushedCommits) {
71
- flags.push('unpushed');
71
+ flags.push("unpushed");
72
72
  }
73
73
  if (target.isAccessible === false) {
74
- flags.push('inaccessible');
74
+ flags.push("inaccessible");
75
75
  }
76
76
 
77
- const flagText = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
77
+ const flagText = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
78
78
 
79
79
  const label = pr
80
80
  ? `${target.branch} - #${pr.number} ${pr.title}${flagText}`
@@ -98,9 +98,9 @@ export function PRCleanupScreen({
98
98
 
99
99
  // Footer actions
100
100
  const footerActions = [
101
- { key: 'enter', description: 'Cleanup' },
102
- { key: 'r', description: 'Refresh' },
103
- { key: 'esc', description: 'Back' },
101
+ { key: "enter", description: "Cleanup" },
102
+ { key: "r", description: "Refresh" },
103
+ { key: "esc", description: "Back" },
104
104
  ];
105
105
 
106
106
  return (
@@ -1,9 +1,9 @@
1
- import React from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
7
 
8
8
  export interface SessionItem {
9
9
  label: string;
@@ -59,8 +59,8 @@ export function SessionSelectorScreen({
59
59
 
60
60
  // Footer actions
61
61
  const footerActions = [
62
- { key: 'enter', description: 'Select' },
63
- { key: 'esc', description: 'Back' },
62
+ { key: "enter", description: "Select" },
63
+ { key: "esc", description: "Back" },
64
64
  ];
65
65
 
66
66
  return (
@@ -1,9 +1,9 @@
1
- import React from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
7
 
8
8
  export interface WorktreeItem {
9
9
  branch: string;
@@ -64,8 +64,8 @@ export function WorktreeManagerScreen({
64
64
 
65
65
  // Footer actions
66
66
  const footerActions = [
67
- { key: 'enter', description: 'Select' },
68
- { key: 'esc', description: 'Back' },
67
+ { key: "enter", description: "Select" },
68
+ { key: "esc", description: "Back" },
69
69
  ];
70
70
 
71
71
  return (
@@ -46,7 +46,9 @@ export function BranchActionSelectorScreen({
46
46
  (mode === "protected" ? "Switch to root branch" : "Use existing branch");
47
47
  const secondaryActionLabel =
48
48
  secondaryLabel ??
49
- (mode === "protected" ? "Create new branch from this branch" : "Create new branch");
49
+ (mode === "protected"
50
+ ? "Create new branch from this branch"
51
+ : "Create new branch");
50
52
 
51
53
  const items: SelectItem[] = [
52
54
  {
@@ -74,15 +76,18 @@ export function BranchActionSelectorScreen({
74
76
 
75
77
  // Footer actions
76
78
  const footerActions = [
77
- { key: 'enter', description: 'Select' },
78
- { key: 'esc', description: 'Back' },
79
+ { key: "enter", description: "Select" },
80
+ { key: "esc", description: "Back" },
79
81
  ];
80
82
 
81
83
  return (
82
84
  <Box flexDirection="column">
83
85
  <Box marginBottom={1}>
84
86
  <Text>
85
- Selected branch: <Text bold color="cyan">{selectedBranch}</Text>
87
+ Selected branch:{" "}
88
+ <Text bold color="cyan">
89
+ {selectedBranch}
90
+ </Text>
86
91
  </Text>
87
92
  </Box>
88
93
  {infoMessage ? (
@@ -5,10 +5,29 @@ export interface WorktreeInfo {
5
5
  isAccessible?: boolean;
6
6
  }
7
7
 
8
+ export type AITool = string;
9
+ export type InferenceLevel = "low" | "medium" | "high" | "xhigh";
10
+
11
+ export interface ModelOption {
12
+ id: string;
13
+ label: string;
14
+ description?: string;
15
+ inferenceLevels?: InferenceLevel[];
16
+ defaultInference?: InferenceLevel;
17
+ isDefault?: boolean;
18
+ }
19
+
8
20
  export interface BranchInfo {
9
21
  name: string;
10
22
  type: "local" | "remote";
11
- branchType: "feature" | "bugfix" | "hotfix" | "release" | "main" | "develop" | "other";
23
+ branchType:
24
+ | "feature"
25
+ | "bugfix"
26
+ | "hotfix"
27
+ | "release"
28
+ | "main"
29
+ | "develop"
30
+ | "other";
12
31
  isCurrent: boolean;
13
32
  description?: string;
14
33
  worktree?: WorktreeInfo;
@@ -154,6 +173,7 @@ export type ScreenType =
154
173
  | "branch-creator"
155
174
  | "branch-action-selector"
156
175
  | "ai-tool-selector"
176
+ | "model-selector"
157
177
  | "session-selector"
158
178
  | "execution-mode-selector"
159
179
  | "batch-merge-progress"
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getModelOptions, getDefaultInferenceForModel } from "./modelOptions.js";
3
+
4
+ const byId = (tool: string) => getModelOptions(tool).map((m) => m.id);
5
+
6
+ describe("modelOptions", () => {
7
+ it("has unique Codex models", () => {
8
+ const ids = byId("codex-cli");
9
+ const unique = new Set(ids);
10
+ expect(unique.size).toBe(ids.length);
11
+ expect(ids).toEqual([
12
+ "gpt-5.1-codex",
13
+ "gpt-5.1-codex-max",
14
+ "gpt-5.1-codex-mini",
15
+ "gpt-5.1",
16
+ ]);
17
+ });
18
+
19
+ it("uses medium as default reasoning for codex-max", () => {
20
+ const codexMax = getModelOptions("codex-cli").find((m) => m.id === "gpt-5.1-codex-max");
21
+ expect(getDefaultInferenceForModel(codexMax)).toBe("medium");
22
+ });
23
+
24
+ it("lists expected Gemini models", () => {
25
+ expect(byId("gemini-cli")).toEqual([
26
+ "gemini-3-pro-preview",
27
+ "gemini-2.5-pro",
28
+ "gemini-2.5-flash",
29
+ "gemini-2.5-flash-lite",
30
+ ]);
31
+ });
32
+
33
+ it("lists expected Qwen models", () => {
34
+ expect(byId("qwen-cli")).toEqual(["coder-model", "vision-model"]);
35
+ });
36
+ });
@@ -0,0 +1,122 @@
1
+ import type { AITool, InferenceLevel, ModelOption } from "../types.js";
2
+
3
+ const CODEX_BASE_LEVELS: InferenceLevel[] = ["high", "medium", "low"];
4
+ const CODEX_MAX_LEVELS: InferenceLevel[] = ["xhigh", "high", "medium", "low"];
5
+
6
+ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
7
+ "claude-code": [
8
+ {
9
+ id: "claude-sonnet-4.5",
10
+ label: "Default (recommended) — Sonnet 4.5",
11
+ description: "Smartest model for daily use (released Sep 29, 2025)",
12
+ isDefault: true,
13
+ },
14
+ {
15
+ id: "claude-opus-4.1",
16
+ label: "Opus 4.1",
17
+ description: "Legacy: Opus 4.1 · reaches usage limits faster",
18
+ },
19
+ {
20
+ id: "claude-haiku-4.5",
21
+ label: "Haiku 4.5",
22
+ description: "Fastest model for simple tasks (released Oct 15, 2025)",
23
+ },
24
+ ],
25
+ "codex-cli": [
26
+ {
27
+ id: "gpt-5.1-codex",
28
+ label: "gpt-5.1-codex",
29
+ description: "Standard Codex model",
30
+ inferenceLevels: CODEX_BASE_LEVELS,
31
+ defaultInference: "high",
32
+ isDefault: true,
33
+ },
34
+ {
35
+ id: "gpt-5.1-codex-max",
36
+ label: "gpt-5.1-codex-max",
37
+ description: "Max performance (xhigh available)",
38
+ inferenceLevels: CODEX_MAX_LEVELS,
39
+ defaultInference: "medium",
40
+ },
41
+ {
42
+ id: "gpt-5.1-codex-mini",
43
+ label: "gpt-5.1-codex-mini",
44
+ description: "Lightweight / cost-saving",
45
+ inferenceLevels: CODEX_BASE_LEVELS,
46
+ defaultInference: "medium",
47
+ },
48
+ {
49
+ id: "gpt-5.1",
50
+ label: "gpt-5.1",
51
+ description: "General-purpose GPT-5.1",
52
+ inferenceLevels: CODEX_BASE_LEVELS,
53
+ defaultInference: "high",
54
+ },
55
+ ],
56
+ "gemini-cli": [
57
+ {
58
+ id: "gemini-3-pro-preview",
59
+ label: "Pro (gemini-3-pro-preview)",
60
+ description:
61
+ "Default Pro. Falls back to gemini-2.5-pro when preview is unavailable.",
62
+ isDefault: true,
63
+ },
64
+ {
65
+ id: "gemini-2.5-pro",
66
+ label: "Pro (gemini-2.5-pro)",
67
+ description: "Stable Pro model for deep reasoning and creativity",
68
+ },
69
+ {
70
+ id: "gemini-2.5-flash",
71
+ label: "Flash (gemini-2.5-flash)",
72
+ description: "Balance of speed and reasoning",
73
+ },
74
+ {
75
+ id: "gemini-2.5-flash-lite",
76
+ label: "Flash-Lite (gemini-2.5-flash-lite)",
77
+ description: "Fastest for simple tasks",
78
+ },
79
+ ],
80
+ "qwen-cli": [
81
+ {
82
+ id: "coder-model",
83
+ label: "Coder Model",
84
+ description:
85
+ "Latest Qwen Coder model (qwen3-coder-plus-2025-09-23) from Alibaba Cloud ModelStudio",
86
+ isDefault: true,
87
+ },
88
+ {
89
+ id: "vision-model",
90
+ label: "Vision Model",
91
+ description:
92
+ "Latest Qwen Vision model (qwen3-vl-plus-2025-09-23) from Alibaba Cloud ModelStudio",
93
+ },
94
+ ],
95
+ };
96
+
97
+ export function getModelOptions(tool: AITool): ModelOption[] {
98
+ return MODEL_OPTIONS[tool] ?? [];
99
+ }
100
+
101
+ export function getDefaultModelOption(tool: AITool): ModelOption | undefined {
102
+ const options = getModelOptions(tool);
103
+ return options.find((opt) => opt.isDefault) ?? options[0];
104
+ }
105
+
106
+ export function getInferenceLevelsForModel(
107
+ model?: ModelOption,
108
+ ): InferenceLevel[] {
109
+ if (!model?.inferenceLevels || model.inferenceLevels.length === 0) {
110
+ return [];
111
+ }
112
+ return model.inferenceLevels;
113
+ }
114
+
115
+ export function getDefaultInferenceForModel(
116
+ model?: ModelOption,
117
+ ): InferenceLevel | undefined {
118
+ if (!model) return undefined;
119
+ if (model.defaultInference) return model.defaultInference;
120
+ const levels = getInferenceLevelsForModel(model);
121
+ return levels[0];
122
+ }