@akiojin/gwt 4.12.0 → 4.12.2
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.
- package/bin/gwt.js +35 -9
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +67 -14
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/App.solid.d.ts.map +1 -1
- package/dist/cli/ui/App.solid.js +40 -8
- package/dist/cli/ui/App.solid.js.map +1 -1
- package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
- package/dist/cli/ui/components/solid/WizardController.js +48 -2
- package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
- package/dist/cli/ui/components/solid/WizardSteps.d.ts +5 -0
- package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
- package/dist/cli/ui/components/solid/WizardSteps.js +29 -6
- package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
- package/dist/cli/ui/utils/installedVersionCache.d.ts +33 -0
- package/dist/cli/ui/utils/installedVersionCache.d.ts.map +1 -0
- package/dist/cli/ui/utils/installedVersionCache.js +59 -0
- package/dist/cli/ui/utils/installedVersionCache.js.map +1 -0
- package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
- package/dist/cli/ui/utils/modelOptions.js +16 -0
- package/dist/cli/ui/utils/modelOptions.js.map +1 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +63 -22
- package/dist/codex.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/reader.d.ts +2 -1
- package/dist/logging/reader.d.ts.map +1 -1
- package/dist/logging/reader.js +37 -1
- package/dist/logging/reader.js.map +1 -1
- package/dist/opentui/index.solid.js +298 -264
- package/dist/services/codingAgentResolver.d.ts.map +1 -1
- package/dist/services/codingAgentResolver.js +8 -6
- package/dist/services/codingAgentResolver.js.map +1 -1
- package/dist/shared/codingAgentConstants.d.ts +3 -0
- package/dist/shared/codingAgentConstants.d.ts.map +1 -1
- package/dist/shared/codingAgentConstants.js +66 -0
- package/dist/shared/codingAgentConstants.js.map +1 -1
- package/dist/utils/bun-runtime.d.ts +12 -0
- package/dist/utils/bun-runtime.d.ts.map +1 -0
- package/dist/utils/bun-runtime.js +13 -0
- package/dist/utils/bun-runtime.js.map +1 -0
- package/dist/utils/session/parsers/codex.d.ts.map +1 -1
- package/dist/utils/session/parsers/codex.js +0 -1
- package/dist/utils/session/parsers/codex.js.map +1 -1
- package/package.json +1 -1
- package/src/claude.ts +79 -15
- package/src/cli/ui/App.solid.tsx +55 -6
- package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +91 -0
- package/src/cli/ui/__tests__/solid/components/WizardController.test.tsx +63 -0
- package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +91 -1
- package/src/cli/ui/components/solid/WizardController.tsx +65 -1
- package/src/cli/ui/components/solid/WizardSteps.tsx +55 -10
- package/src/cli/ui/utils/__tests__/installedVersionCache.test.ts +46 -0
- package/src/cli/ui/utils/installedVersionCache.ts +84 -0
- package/src/cli/ui/utils/modelOptions.test.ts +6 -0
- package/src/cli/ui/utils/modelOptions.ts +16 -0
- package/src/codex.ts +78 -22
- package/src/index.ts +5 -0
- package/src/logging/reader.ts +48 -1
- package/src/services/codingAgentResolver.ts +12 -5
- package/src/shared/codingAgentConstants.ts +73 -0
- package/src/utils/bun-runtime.ts +29 -0
- package/src/utils/session/parsers/codex.ts +0 -1
|
@@ -486,6 +486,97 @@ describe("AppSolid unsafe selection confirm", () => {
|
|
|
486
486
|
}
|
|
487
487
|
});
|
|
488
488
|
|
|
489
|
+
it("shows confirm when safety check is pending", async () => {
|
|
490
|
+
let resolveStatus: ((value: unknown[]) => void) | null = null;
|
|
491
|
+
const pendingPromise = new Promise<unknown[]>((resolve) => {
|
|
492
|
+
resolveStatus = resolve;
|
|
493
|
+
});
|
|
494
|
+
const getCleanupStatusMock = mock(async () => pendingPromise);
|
|
495
|
+
|
|
496
|
+
mock.module?.("../../../../worktree.js", () => ({
|
|
497
|
+
listAdditionalWorktrees: mock(async () => []),
|
|
498
|
+
repairWorktrees: mock(async () => ({
|
|
499
|
+
repairedCount: 0,
|
|
500
|
+
failedCount: 0,
|
|
501
|
+
failures: [],
|
|
502
|
+
})),
|
|
503
|
+
removeWorktree: mock(async () => {}),
|
|
504
|
+
getCleanupStatus: getCleanupStatusMock,
|
|
505
|
+
isProtectedBranchName: mock(() => false),
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
mock.module?.("../../../../git.js", () => ({
|
|
509
|
+
getRepositoryRoot: mock(async () => "/repo"),
|
|
510
|
+
getAllBranches: mock(async () => []),
|
|
511
|
+
getLocalBranches: mock(async () => []),
|
|
512
|
+
getCurrentBranch: mock(async () => "main"),
|
|
513
|
+
deleteBranch: mock(async () => {}),
|
|
514
|
+
}));
|
|
515
|
+
|
|
516
|
+
mock.module?.("../../../../config/index.js", () => ({
|
|
517
|
+
getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
|
|
518
|
+
getLastToolUsageMap: mock(async () => new Map()),
|
|
519
|
+
loadSession: mock(async () => null),
|
|
520
|
+
}));
|
|
521
|
+
|
|
522
|
+
mock.module?.("../../../../config/tools.js", () => ({
|
|
523
|
+
getAllCodingAgents: mock(async () => [
|
|
524
|
+
{ id: "codex-cli", displayName: "Codex CLI" },
|
|
525
|
+
]),
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
mock.module?.("../../../../config/profiles.js", () => ({
|
|
529
|
+
loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
|
|
530
|
+
createProfile: mock(async () => {}),
|
|
531
|
+
updateProfile: mock(async () => {}),
|
|
532
|
+
deleteProfile: mock(async () => {}),
|
|
533
|
+
setActiveProfile: mock(async () => {}),
|
|
534
|
+
}));
|
|
535
|
+
|
|
536
|
+
const { AppSolid } = await import("../../App.solid.js");
|
|
537
|
+
|
|
538
|
+
const branch = createBranch({
|
|
539
|
+
name: "feature/pending",
|
|
540
|
+
label: "feature/pending",
|
|
541
|
+
value: "feature/pending",
|
|
542
|
+
});
|
|
543
|
+
const stats = makeStats({ localCount: 1, worktreeCount: 1 });
|
|
544
|
+
|
|
545
|
+
const testSetup = await testRender(
|
|
546
|
+
() => (
|
|
547
|
+
<AppSolid
|
|
548
|
+
branches={[branch]}
|
|
549
|
+
stats={stats}
|
|
550
|
+
version={null}
|
|
551
|
+
toolStatuses={[]}
|
|
552
|
+
/>
|
|
553
|
+
),
|
|
554
|
+
{ width: 80, height: 24 },
|
|
555
|
+
);
|
|
556
|
+
await testSetup.renderOnce();
|
|
557
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
558
|
+
await testSetup.renderOnce();
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
await testSetup.mockInput.typeText(" ");
|
|
562
|
+
await testSetup.renderOnce();
|
|
563
|
+
|
|
564
|
+
let frame = testSetup.captureCharFrame();
|
|
565
|
+
expect(frame).toContain("Safety check in progress. Select anyway?");
|
|
566
|
+
|
|
567
|
+
await testSetup.mockInput.typeText("n");
|
|
568
|
+
await testSetup.renderOnce();
|
|
569
|
+
|
|
570
|
+
frame = testSetup.captureCharFrame();
|
|
571
|
+
expect(frame).toContain("[ ] w");
|
|
572
|
+
expect(frame).toContain("feature/pending");
|
|
573
|
+
expect(frame).not.toContain("Safety check in progress. Select anyway?");
|
|
574
|
+
} finally {
|
|
575
|
+
resolveStatus?.([]);
|
|
576
|
+
testSetup.renderer.destroy();
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
489
580
|
it("does not propagate Enter from confirm to branch selection", async () => {
|
|
490
581
|
const getCleanupStatusMock = mock(async () => [
|
|
491
582
|
{
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
3
|
+
import { testRender } from "@opentui/solid";
|
|
4
|
+
import type { ToolSessionEntry } from "../../../../config/index.js";
|
|
5
|
+
|
|
6
|
+
mock.module("../../../utils/versionFetcher.js", () => ({
|
|
7
|
+
fetchInstalledVersionForAgent: mock(async () => null),
|
|
8
|
+
createInstalledOption: (version: string) => ({
|
|
9
|
+
label: `installed (${version})`,
|
|
10
|
+
value: "installed",
|
|
11
|
+
}),
|
|
12
|
+
versionInfoToSelectItem: (v: { version: string }) => ({
|
|
13
|
+
label: v.version,
|
|
14
|
+
value: v.version,
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe("WizardController", () => {
|
|
19
|
+
it("keeps version selection visible after agent select", async () => {
|
|
20
|
+
const history: ToolSessionEntry[] = [];
|
|
21
|
+
|
|
22
|
+
const { WizardController } =
|
|
23
|
+
await import("../../../components/solid/WizardController.js");
|
|
24
|
+
|
|
25
|
+
const testSetup = await testRender(
|
|
26
|
+
() => (
|
|
27
|
+
<WizardController
|
|
28
|
+
visible
|
|
29
|
+
selectedBranchName="feature/test"
|
|
30
|
+
history={history}
|
|
31
|
+
onClose={() => {}}
|
|
32
|
+
onComplete={() => {}}
|
|
33
|
+
onResume={() => {}}
|
|
34
|
+
onStartNew={() => {}}
|
|
35
|
+
/>
|
|
36
|
+
),
|
|
37
|
+
{ width: 80, height: 24 },
|
|
38
|
+
);
|
|
39
|
+
await testSetup.renderOnce();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
let frame = testSetup.captureCharFrame();
|
|
43
|
+
expect(frame).toContain("What would you like to do?");
|
|
44
|
+
|
|
45
|
+
// Open existing worktree
|
|
46
|
+
testSetup.mockInput.pressEnter();
|
|
47
|
+
await testSetup.renderOnce();
|
|
48
|
+
|
|
49
|
+
frame = testSetup.captureCharFrame();
|
|
50
|
+
expect(frame).toContain("Select coding agent:");
|
|
51
|
+
|
|
52
|
+
// Select default agent (Enter)
|
|
53
|
+
testSetup.mockInput.pressEnter();
|
|
54
|
+
await testSetup.renderOnce();
|
|
55
|
+
|
|
56
|
+
frame = testSetup.captureCharFrame();
|
|
57
|
+
expect(frame).toContain("Select version:");
|
|
58
|
+
expect(frame).not.toContain("Select Model:");
|
|
59
|
+
} finally {
|
|
60
|
+
testSetup.renderer.destroy();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
3
3
|
import { testRender } from "@opentui/solid";
|
|
4
4
|
import {
|
|
5
5
|
BranchTypeStep,
|
|
6
6
|
BranchNameStep,
|
|
7
7
|
AgentSelectStep,
|
|
8
|
+
VersionSelectStep,
|
|
8
9
|
ModelSelectStep,
|
|
9
10
|
ReasoningLevelStep,
|
|
10
11
|
ExecutionModeStep,
|
|
11
12
|
SkipPermissionsStep,
|
|
12
13
|
} from "../../../components/solid/WizardSteps.js";
|
|
14
|
+
import {
|
|
15
|
+
clearInstalledVersionCache,
|
|
16
|
+
setInstalledVersionCache,
|
|
17
|
+
} from "../../../utils/installedVersionCache.js";
|
|
18
|
+
import {
|
|
19
|
+
clearVersionCache,
|
|
20
|
+
setVersionCache,
|
|
21
|
+
} from "../../../utils/versionCache.js";
|
|
13
22
|
|
|
14
23
|
// T405: ブランチタイプ選択ステップのテスト
|
|
15
24
|
describe("BranchTypeStep", () => {
|
|
@@ -128,6 +137,65 @@ describe("AgentSelectStep", () => {
|
|
|
128
137
|
});
|
|
129
138
|
});
|
|
130
139
|
|
|
140
|
+
// T407a: バージョン選択ステップのテスト
|
|
141
|
+
describe("VersionSelectStep", () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
clearInstalledVersionCache();
|
|
144
|
+
clearVersionCache();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("renders installed option from cache", async () => {
|
|
148
|
+
setInstalledVersionCache("claude-code", {
|
|
149
|
+
version: "1.2.3",
|
|
150
|
+
path: "/usr/local/bin/claude",
|
|
151
|
+
});
|
|
152
|
+
setVersionCache("claude-code", []);
|
|
153
|
+
|
|
154
|
+
const testSetup = await testRender(
|
|
155
|
+
() => (
|
|
156
|
+
<VersionSelectStep
|
|
157
|
+
agentId="claude-code"
|
|
158
|
+
onSelect={() => {}}
|
|
159
|
+
onBack={() => {}}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
{ width: 60, height: 20 },
|
|
163
|
+
);
|
|
164
|
+
await testSetup.renderOnce();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const frame = testSetup.captureCharFrame();
|
|
168
|
+
expect(frame).toContain("Select version");
|
|
169
|
+
expect(frame).toContain("installed@1.2.3");
|
|
170
|
+
expect(frame).toContain("latest");
|
|
171
|
+
} finally {
|
|
172
|
+
testSetup.renderer.destroy();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("does not render installed option when cache is empty", async () => {
|
|
177
|
+
const testSetup = await testRender(
|
|
178
|
+
() => (
|
|
179
|
+
<VersionSelectStep
|
|
180
|
+
agentId="claude-code"
|
|
181
|
+
onSelect={() => {}}
|
|
182
|
+
onBack={() => {}}
|
|
183
|
+
/>
|
|
184
|
+
),
|
|
185
|
+
{ width: 60, height: 20 },
|
|
186
|
+
);
|
|
187
|
+
await testSetup.renderOnce();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const frame = testSetup.captureCharFrame();
|
|
191
|
+
expect(frame).toContain("latest");
|
|
192
|
+
expect(frame).not.toContain("installed@");
|
|
193
|
+
} finally {
|
|
194
|
+
testSetup.renderer.destroy();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
131
199
|
// T408: モデル選択ステップのテスト
|
|
132
200
|
describe("ModelSelectStep", () => {
|
|
133
201
|
it("renders model options for Claude Code", async () => {
|
|
@@ -150,6 +218,28 @@ describe("ModelSelectStep", () => {
|
|
|
150
218
|
testSetup.renderer.destroy();
|
|
151
219
|
}
|
|
152
220
|
});
|
|
221
|
+
|
|
222
|
+
it("renders model options for OpenCode", async () => {
|
|
223
|
+
const testSetup = await testRender(
|
|
224
|
+
() => (
|
|
225
|
+
<ModelSelectStep
|
|
226
|
+
agentId="opencode"
|
|
227
|
+
onSelect={() => {}}
|
|
228
|
+
onBack={() => {}}
|
|
229
|
+
/>
|
|
230
|
+
),
|
|
231
|
+
{ width: 60, height: 20 },
|
|
232
|
+
);
|
|
233
|
+
await testSetup.renderOnce();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const frame = testSetup.captureCharFrame();
|
|
237
|
+
expect(frame).toContain("Default (Auto)");
|
|
238
|
+
expect(frame).toContain("Custom");
|
|
239
|
+
} finally {
|
|
240
|
+
testSetup.renderer.destroy();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
153
243
|
});
|
|
154
244
|
|
|
155
245
|
// T409: 推論レベル選択ステップ(Codexのみ)のテスト
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createSignal,
|
|
4
|
+
createEffect,
|
|
5
|
+
createMemo,
|
|
6
|
+
onCleanup,
|
|
7
|
+
Show,
|
|
8
|
+
} from "solid-js";
|
|
3
9
|
import { useKeyboard } from "@opentui/solid";
|
|
4
10
|
import type { ToolSessionEntry } from "../../../../config/index.js";
|
|
5
11
|
import type { CodingAgentId, InferenceLevel } from "../../types.js";
|
|
@@ -13,6 +19,7 @@ import {
|
|
|
13
19
|
AgentSelectStep,
|
|
14
20
|
VersionSelectStep,
|
|
15
21
|
ModelSelectStep,
|
|
22
|
+
ModelInputStep,
|
|
16
23
|
ReasoningLevelStep,
|
|
17
24
|
ExecutionModeStep,
|
|
18
25
|
SkipPermissionsStep,
|
|
@@ -53,6 +60,7 @@ type WizardStep =
|
|
|
53
60
|
| "agent-select"
|
|
54
61
|
| "version-select"
|
|
55
62
|
| "model-select"
|
|
63
|
+
| "model-input"
|
|
56
64
|
| "reasoning-level"
|
|
57
65
|
| "execution-mode"
|
|
58
66
|
| "skip-permissions";
|
|
@@ -87,9 +95,11 @@ export function WizardController(props: WizardControllerProps) {
|
|
|
87
95
|
>(undefined);
|
|
88
96
|
const [executionMode, setExecutionMode] =
|
|
89
97
|
createSignal<ExecutionMode>("normal");
|
|
98
|
+
const [versionSelectionReady, setVersionSelectionReady] = createSignal(false);
|
|
90
99
|
|
|
91
100
|
// キー伝播防止: ステップ遷移直後は focused を無効にする
|
|
92
101
|
const [isTransitioning, setIsTransitioning] = createSignal(true);
|
|
102
|
+
let versionSelectionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
93
103
|
|
|
94
104
|
// Reset state when wizard becomes visible
|
|
95
105
|
function getInitialStep(): WizardStep {
|
|
@@ -168,6 +178,28 @@ export function WizardController(props: WizardControllerProps) {
|
|
|
168
178
|
startTransition();
|
|
169
179
|
};
|
|
170
180
|
|
|
181
|
+
createEffect(() => {
|
|
182
|
+
const currentStep = step();
|
|
183
|
+
if (versionSelectionTimer) {
|
|
184
|
+
clearTimeout(versionSelectionTimer);
|
|
185
|
+
versionSelectionTimer = null;
|
|
186
|
+
}
|
|
187
|
+
if (currentStep === "version-select") {
|
|
188
|
+
setVersionSelectionReady(false);
|
|
189
|
+
versionSelectionTimer = setTimeout(() => {
|
|
190
|
+
setVersionSelectionReady(true);
|
|
191
|
+
}, 50);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
setVersionSelectionReady(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
onCleanup(() => {
|
|
198
|
+
if (versionSelectionTimer) {
|
|
199
|
+
clearTimeout(versionSelectionTimer);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
171
203
|
// Determine if reasoning level step is needed
|
|
172
204
|
const needsReasoningLevel = createMemo(() => {
|
|
173
205
|
return selectedAgent() === "codex-cli";
|
|
@@ -213,12 +245,20 @@ export function WizardController(props: WizardControllerProps) {
|
|
|
213
245
|
};
|
|
214
246
|
|
|
215
247
|
const handleVersionSelect = (version: string) => {
|
|
248
|
+
if (!versionSelectionReady() || step() !== "version-select") {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
216
251
|
// "installed" を明示的に保存し、未指定時は後方互換で "latest" にフォールバックできるようにする
|
|
217
252
|
setSelectedVersion(version);
|
|
218
253
|
goToStep("model-select");
|
|
219
254
|
};
|
|
220
255
|
|
|
221
256
|
const handleModelSelect = (model: string) => {
|
|
257
|
+
const agent = selectedAgent();
|
|
258
|
+
if (agent === "opencode" && model === "__custom__") {
|
|
259
|
+
goToStep("model-input");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
222
262
|
setSelectedModel(model);
|
|
223
263
|
if (needsReasoningLevel()) {
|
|
224
264
|
goToStep("reasoning-level");
|
|
@@ -227,6 +267,19 @@ export function WizardController(props: WizardControllerProps) {
|
|
|
227
267
|
}
|
|
228
268
|
};
|
|
229
269
|
|
|
270
|
+
const handleModelInputSubmit = (value: string) => {
|
|
271
|
+
const trimmed = value.trim();
|
|
272
|
+
if (!trimmed) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
setSelectedModel(trimmed);
|
|
276
|
+
if (needsReasoningLevel()) {
|
|
277
|
+
goToStep("reasoning-level");
|
|
278
|
+
} else {
|
|
279
|
+
goToStep("execution-mode");
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
230
283
|
const handleReasoningLevelSelect = (level: string) => {
|
|
231
284
|
setReasoningLevel(level as InferenceLevel);
|
|
232
285
|
goToStep("execution-mode");
|
|
@@ -346,6 +399,17 @@ export function WizardController(props: WizardControllerProps) {
|
|
|
346
399
|
);
|
|
347
400
|
}
|
|
348
401
|
|
|
402
|
+
if (currentStep === "model-input") {
|
|
403
|
+
return (
|
|
404
|
+
<ModelInputStep
|
|
405
|
+
agentId={selectedAgent() ?? "claude-code"}
|
|
406
|
+
onSubmit={handleModelInputSubmit}
|
|
407
|
+
onBack={goBack}
|
|
408
|
+
focused={focused}
|
|
409
|
+
/>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
349
413
|
if (currentStep === "reasoning-level") {
|
|
350
414
|
return (
|
|
351
415
|
<ReasoningLevelStep
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { TextAttributes } from "@opentui/core";
|
|
3
3
|
import type { SelectRenderable } from "@opentui/core";
|
|
4
4
|
import { useKeyboard } from "@opentui/solid";
|
|
5
|
-
import { createEffect,
|
|
5
|
+
import { createEffect, createSignal } from "solid-js";
|
|
6
6
|
import { SelectInput, type SelectInputItem } from "./SelectInput.js";
|
|
7
7
|
import { TextInput } from "./TextInput.js";
|
|
8
8
|
import { getModelOptions } from "../../utils/modelOptions.js";
|
|
@@ -12,10 +12,10 @@ import { getAgentTerminalColor } from "../../../../utils/coding-agent-colors.js"
|
|
|
12
12
|
import { getVersionCache } from "../../utils/versionCache.js";
|
|
13
13
|
import { selectionStyle } from "../../core/theme.js";
|
|
14
14
|
import {
|
|
15
|
-
fetchInstalledVersionForAgent,
|
|
16
15
|
versionInfoToSelectItem,
|
|
17
16
|
createInstalledOption,
|
|
18
17
|
} from "../../utils/versionFetcher.js";
|
|
18
|
+
import { getInstalledVersionCache } from "../../utils/installedVersionCache.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* WizardSteps - ウィザードの各ステップコンポーネント
|
|
@@ -441,6 +441,54 @@ export function ModelSelectStep(props: ModelSelectStepProps) {
|
|
|
441
441
|
);
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
// T408b: カスタムモデル入力ステップ
|
|
445
|
+
export interface ModelInputStepProps extends StepProps {
|
|
446
|
+
agentId: CodingAgentId;
|
|
447
|
+
onSubmit: (value: string) => void;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function ModelInputStep(props: ModelInputStepProps) {
|
|
451
|
+
const [value, setValue] = createSignal("");
|
|
452
|
+
const scroll = useWizardScroll();
|
|
453
|
+
const placeholder = props.agentId === "opencode" ? "provider/model" : "model";
|
|
454
|
+
|
|
455
|
+
createEffect(() => {
|
|
456
|
+
if (props.focused === false) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (!scroll) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
scroll.ensureLineVisible(2);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const handleSubmit = (next: string) => {
|
|
466
|
+
const trimmed = next.trim();
|
|
467
|
+
if (!trimmed) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
props.onSubmit(trimmed);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<box flexDirection="column">
|
|
475
|
+
<text fg="cyan" attributes={TextAttributes.BOLD}>
|
|
476
|
+
Enter custom model:
|
|
477
|
+
</text>
|
|
478
|
+
<text> </text>
|
|
479
|
+
<TextInput
|
|
480
|
+
value={value()}
|
|
481
|
+
onChange={setValue}
|
|
482
|
+
onSubmit={handleSubmit}
|
|
483
|
+
placeholder={placeholder}
|
|
484
|
+
focused={props.focused ?? true}
|
|
485
|
+
/>
|
|
486
|
+
<text> </text>
|
|
487
|
+
<text attributes={TextAttributes.DIM}>[Enter] Submit [Esc] Back</text>
|
|
488
|
+
</box>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
444
492
|
// T409: 推論レベル選択ステップ(Codexのみ)
|
|
445
493
|
export interface ReasoningLevelStepProps extends StepProps {
|
|
446
494
|
onSelect: (level: string) => void;
|
|
@@ -642,14 +690,11 @@ export function VersionSelectStep(props: VersionSelectStepProps) {
|
|
|
642
690
|
return cached.map(versionInfoToSelectItem);
|
|
643
691
|
};
|
|
644
692
|
|
|
645
|
-
//
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return installed ? createInstalledOption(installed) : null;
|
|
651
|
-
},
|
|
652
|
-
);
|
|
693
|
+
// インストール済み情報は起動時にキャッシュ済み(FR-017)
|
|
694
|
+
const installedOption = () => {
|
|
695
|
+
const installed = getInstalledVersionCache(props.agentId);
|
|
696
|
+
return installed ? createInstalledOption(installed) : null;
|
|
697
|
+
};
|
|
653
698
|
|
|
654
699
|
// 全オプション(installed + latest + cached versions)
|
|
655
700
|
const allOptions = (): SelectInputItem[] => {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
clearInstalledVersionCache,
|
|
4
|
+
getInstalledVersionCache,
|
|
5
|
+
prefetchInstalledVersions,
|
|
6
|
+
setInstalledVersionCache,
|
|
7
|
+
} from "../installedVersionCache.js";
|
|
8
|
+
|
|
9
|
+
describe("installedVersionCache", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
clearInstalledVersionCache();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("stores and retrieves installed versions", () => {
|
|
15
|
+
setInstalledVersionCache("claude-code", {
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
path: "/usr/local/bin/claude",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const cached = getInstalledVersionCache("claude-code");
|
|
21
|
+
expect(cached).toEqual({
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
path: "/usr/local/bin/claude",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("prefetches installed versions with custom fetcher", async () => {
|
|
28
|
+
await prefetchInstalledVersions(["claude-code", "codex-cli"], async (id) =>
|
|
29
|
+
id === "claude-code" ? { version: "2.0.0", path: "/opt/claude" } : null,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(getInstalledVersionCache("claude-code")).toEqual({
|
|
33
|
+
version: "2.0.0",
|
|
34
|
+
path: "/opt/claude",
|
|
35
|
+
});
|
|
36
|
+
expect(getInstalledVersionCache("codex-cli")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("stores null when fetcher throws", async () => {
|
|
40
|
+
await prefetchInstalledVersions(["gemini-cli"], async () => {
|
|
41
|
+
throw new Error("boom");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(getInstalledVersionCache("gemini-cli")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installed version cache for coding agents (FR-017)
|
|
3
|
+
*
|
|
4
|
+
* Caches locally installed agent versions detected at startup
|
|
5
|
+
* to avoid async fetch during wizard navigation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { InstalledVersionInfo } from "./versionFetcher.js";
|
|
9
|
+
import { fetchInstalledVersionForAgent } from "./versionFetcher.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* In-memory cache for installed versions
|
|
13
|
+
* Maps agentId -> InstalledVersionInfo | null
|
|
14
|
+
*/
|
|
15
|
+
const installedVersionCache = new Map<string, InstalledVersionInfo | null>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Type for installed version fetch function (injectable for testing)
|
|
19
|
+
*/
|
|
20
|
+
export type InstalledVersionFetcher = (
|
|
21
|
+
agentId: string,
|
|
22
|
+
) => Promise<InstalledVersionInfo | null>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get cached installed version for an agent
|
|
26
|
+
* @returns Cached info or null if not installed/unknown
|
|
27
|
+
*/
|
|
28
|
+
export function getInstalledVersionCache(
|
|
29
|
+
agentId: string,
|
|
30
|
+
): InstalledVersionInfo | null {
|
|
31
|
+
const cached = installedVersionCache.get(agentId);
|
|
32
|
+
return cached ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set installed version cache (for testing or direct population)
|
|
37
|
+
*/
|
|
38
|
+
export function setInstalledVersionCache(
|
|
39
|
+
agentId: string,
|
|
40
|
+
installed: InstalledVersionInfo | null,
|
|
41
|
+
): void {
|
|
42
|
+
installedVersionCache.set(agentId, installed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clear installed version cache
|
|
47
|
+
*/
|
|
48
|
+
export function clearInstalledVersionCache(): void {
|
|
49
|
+
installedVersionCache.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Prefetch installed versions for multiple agents in parallel
|
|
54
|
+
* This should be called at application startup
|
|
55
|
+
*
|
|
56
|
+
* @param agentIds - List of agent IDs to prefetch
|
|
57
|
+
* @param fetchFn - Optional custom fetch function (for testing)
|
|
58
|
+
*/
|
|
59
|
+
export async function prefetchInstalledVersions(
|
|
60
|
+
agentIds: string[],
|
|
61
|
+
fetchFn?: InstalledVersionFetcher,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const fetcher =
|
|
64
|
+
fetchFn ??
|
|
65
|
+
(async (agentId: string) => fetchInstalledVersionForAgent(agentId));
|
|
66
|
+
|
|
67
|
+
const results = await Promise.allSettled(
|
|
68
|
+
agentIds.map(async (agentId) => {
|
|
69
|
+
try {
|
|
70
|
+
const installed = await fetcher(agentId);
|
|
71
|
+
setInstalledVersionCache(agentId, installed);
|
|
72
|
+
} catch {
|
|
73
|
+
setInstalledVersionCache(agentId, null);
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Swallow any rejections to keep startup non-blocking
|
|
79
|
+
for (const result of results) {
|
|
80
|
+
if (result.status === "rejected") {
|
|
81
|
+
// no-op
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -75,6 +75,12 @@ describe("modelOptions", () => {
|
|
|
75
75
|
]);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
it("includes OpenCode default and custom options", () => {
|
|
79
|
+
expect(byId("opencode")).toEqual(["", "__custom__"]);
|
|
80
|
+
const defaultModel = getDefaultModelOption("opencode");
|
|
81
|
+
expect(defaultModel?.id).toBe("");
|
|
82
|
+
});
|
|
83
|
+
|
|
78
84
|
it("normalizes known Claude model typos and casing", () => {
|
|
79
85
|
expect(normalizeModelId("claude-code", "opuss")).toBe("opus");
|
|
80
86
|
expect(normalizeModelId("claude-code", "Opus")).toBe("opus");
|
|
@@ -103,6 +103,19 @@ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
|
|
|
103
103
|
description: "Fastest for simple tasks",
|
|
104
104
|
},
|
|
105
105
|
],
|
|
106
|
+
opencode: [
|
|
107
|
+
{
|
|
108
|
+
id: "",
|
|
109
|
+
label: "Default (Auto)",
|
|
110
|
+
description: "Use OpenCode default model",
|
|
111
|
+
isDefault: true,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "__custom__",
|
|
115
|
+
label: "Custom (provider/model)",
|
|
116
|
+
description: "Enter a provider/model identifier",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
106
119
|
};
|
|
107
120
|
|
|
108
121
|
export function getModelOptions(tool: CodingAgentId): ModelOption[] {
|
|
@@ -144,6 +157,9 @@ export function normalizeModelId(
|
|
|
144
157
|
if (model === null || model === undefined) return model ?? null;
|
|
145
158
|
const trimmed = model.trim();
|
|
146
159
|
if (!trimmed) return null;
|
|
160
|
+
if (tool === "opencode" && trimmed === "__custom__") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
147
163
|
if (tool === "claude-code") {
|
|
148
164
|
const lower = trimmed.toLowerCase();
|
|
149
165
|
if (lower === "opuss") return "opus";
|