@akiojin/gwt 4.11.6 → 4.12.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 (199) hide show
  1. package/bin/gwt.js +36 -10
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +81 -24
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/App.solid.d.ts.map +1 -1
  7. package/dist/cli/ui/App.solid.js +280 -50
  8. package/dist/cli/ui/App.solid.js.map +1 -1
  9. package/dist/cli/ui/components/solid/QuickStartStep.d.ts.map +1 -1
  10. package/dist/cli/ui/components/solid/QuickStartStep.js +35 -22
  11. package/dist/cli/ui/components/solid/QuickStartStep.js.map +1 -1
  12. package/dist/cli/ui/components/solid/SelectInput.d.ts.map +1 -1
  13. package/dist/cli/ui/components/solid/SelectInput.js +2 -1
  14. package/dist/cli/ui/components/solid/SelectInput.js.map +1 -1
  15. package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
  16. package/dist/cli/ui/components/solid/WizardController.js +67 -13
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts +5 -0
  19. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  20. package/dist/cli/ui/components/solid/WizardSteps.js +50 -70
  21. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  22. package/dist/cli/ui/core/theme.d.ts +9 -0
  23. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  24. package/dist/cli/ui/core/theme.js +21 -0
  25. package/dist/cli/ui/core/theme.js.map +1 -1
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  27. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  29. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  33. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  36. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  39. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  40. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  43. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  46. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  47. package/dist/cli/ui/types.d.ts +1 -0
  48. package/dist/cli/ui/types.d.ts.map +1 -1
  49. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  50. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  51. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  52. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  53. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  54. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  55. package/dist/cli/ui/utils/continueSession.js +61 -3
  56. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  57. package/dist/cli/ui/utils/installedVersionCache.d.ts +33 -0
  58. package/dist/cli/ui/utils/installedVersionCache.d.ts.map +1 -0
  59. package/dist/cli/ui/utils/installedVersionCache.js +59 -0
  60. package/dist/cli/ui/utils/installedVersionCache.js.map +1 -0
  61. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  62. package/dist/cli/ui/utils/modelOptions.js +16 -0
  63. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  64. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  65. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  66. package/dist/cli/ui/utils/versionCache.js +70 -0
  67. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  68. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  69. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  70. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  71. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  72. package/dist/codex.d.ts +1 -0
  73. package/dist/codex.d.ts.map +1 -1
  74. package/dist/codex.js +95 -25
  75. package/dist/codex.js.map +1 -1
  76. package/dist/config/index.d.ts.map +1 -1
  77. package/dist/config/index.js +10 -1
  78. package/dist/config/index.js.map +1 -1
  79. package/dist/gemini.d.ts +1 -0
  80. package/dist/gemini.d.ts.map +1 -1
  81. package/dist/gemini.js +36 -3
  82. package/dist/gemini.js.map +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +35 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/launcher.d.ts.map +1 -1
  87. package/dist/launcher.js +43 -8
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/logging/agentOutput.d.ts +21 -0
  90. package/dist/logging/agentOutput.d.ts.map +1 -0
  91. package/dist/logging/agentOutput.js +164 -0
  92. package/dist/logging/agentOutput.js.map +1 -0
  93. package/dist/logging/formatter.d.ts.map +1 -1
  94. package/dist/logging/formatter.js +18 -4
  95. package/dist/logging/formatter.js.map +1 -1
  96. package/dist/logging/logger.d.ts.map +1 -1
  97. package/dist/logging/logger.js +2 -0
  98. package/dist/logging/logger.js.map +1 -1
  99. package/dist/logging/reader.d.ts +22 -0
  100. package/dist/logging/reader.d.ts.map +1 -1
  101. package/dist/logging/reader.js +116 -1
  102. package/dist/logging/reader.js.map +1 -1
  103. package/dist/opentui/index.solid.js +2575 -888
  104. package/dist/services/codingAgentResolver.d.ts.map +1 -1
  105. package/dist/services/codingAgentResolver.js +8 -6
  106. package/dist/services/codingAgentResolver.js.map +1 -1
  107. package/dist/services/dependency-installer.js +2 -2
  108. package/dist/services/dependency-installer.js.map +1 -1
  109. package/dist/shared/codingAgentConstants.d.ts +3 -0
  110. package/dist/shared/codingAgentConstants.d.ts.map +1 -1
  111. package/dist/shared/codingAgentConstants.js +66 -0
  112. package/dist/shared/codingAgentConstants.js.map +1 -1
  113. package/dist/utils/bun-runtime.d.ts +12 -0
  114. package/dist/utils/bun-runtime.d.ts.map +1 -0
  115. package/dist/utils/bun-runtime.js +13 -0
  116. package/dist/utils/bun-runtime.js.map +1 -0
  117. package/dist/utils/session/common.d.ts +8 -0
  118. package/dist/utils/session/common.d.ts.map +1 -1
  119. package/dist/utils/session/common.js +22 -0
  120. package/dist/utils/session/common.js.map +1 -1
  121. package/dist/utils/session/parsers/claude.d.ts +10 -4
  122. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  123. package/dist/utils/session/parsers/claude.js +64 -18
  124. package/dist/utils/session/parsers/claude.js.map +1 -1
  125. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  126. package/dist/utils/session/parsers/codex.js +47 -28
  127. package/dist/utils/session/parsers/codex.js.map +1 -1
  128. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  129. package/dist/utils/session/parsers/gemini.js +43 -6
  130. package/dist/utils/session/parsers/gemini.js.map +1 -1
  131. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  132. package/dist/utils/session/parsers/opencode.js +43 -6
  133. package/dist/utils/session/parsers/opencode.js.map +1 -1
  134. package/dist/utils/session/types.d.ts +7 -0
  135. package/dist/utils/session/types.d.ts.map +1 -1
  136. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  137. package/dist/worktree.d.ts +4 -1
  138. package/dist/worktree.d.ts.map +1 -1
  139. package/dist/worktree.js +21 -15
  140. package/dist/worktree.js.map +1 -1
  141. package/package.json +2 -1
  142. package/src/claude.ts +99 -28
  143. package/src/cli/ui/App.solid.tsx +373 -51
  144. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +921 -1
  145. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  146. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  147. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  148. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  149. package/src/cli/ui/__tests__/solid/components/WizardController.test.tsx +71 -0
  150. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +95 -2
  151. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  152. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  153. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  154. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  155. package/src/cli/ui/components/solid/WizardController.tsx +85 -12
  156. package/src/cli/ui/components/solid/WizardSteps.tsx +78 -90
  157. package/src/cli/ui/core/theme.ts +32 -0
  158. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  159. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  160. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  161. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  162. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  163. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  164. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  165. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  166. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  167. package/src/cli/ui/types.ts +1 -0
  168. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  169. package/src/cli/ui/utils/__tests__/installedVersionCache.test.ts +46 -0
  170. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  171. package/src/cli/ui/utils/continueSession.ts +90 -3
  172. package/src/cli/ui/utils/installedVersionCache.ts +84 -0
  173. package/src/cli/ui/utils/modelOptions.test.ts +6 -0
  174. package/src/cli/ui/utils/modelOptions.ts +16 -0
  175. package/src/cli/ui/utils/versionCache.ts +93 -0
  176. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  177. package/src/codex.ts +124 -26
  178. package/src/config/__tests__/saveSession.test.ts +2 -2
  179. package/src/config/index.ts +11 -1
  180. package/src/gemini.ts +50 -4
  181. package/src/index.test.ts +16 -10
  182. package/src/index.ts +41 -1
  183. package/src/launcher.ts +49 -8
  184. package/src/logging/agentOutput.ts +216 -0
  185. package/src/logging/formatter.ts +23 -4
  186. package/src/logging/logger.ts +2 -0
  187. package/src/logging/reader.ts +165 -1
  188. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  189. package/src/services/codingAgentResolver.ts +12 -5
  190. package/src/services/dependency-installer.ts +2 -2
  191. package/src/shared/codingAgentConstants.ts +73 -0
  192. package/src/utils/bun-runtime.ts +29 -0
  193. package/src/utils/session/common.ts +28 -0
  194. package/src/utils/session/parsers/claude.ts +79 -29
  195. package/src/utils/session/parsers/codex.ts +49 -26
  196. package/src/utils/session/parsers/gemini.ts +46 -5
  197. package/src/utils/session/parsers/opencode.ts +46 -5
  198. package/src/utils/session/types.ts +4 -0
  199. package/src/worktree.ts +28 -15
@@ -7,6 +7,31 @@ import type { BranchInfo } from "../../types.js";
7
7
 
8
8
  // TDD: テストを先に書き、実装は後から行う
9
9
 
10
+ const LOCAL_DATE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
11
+ year: "numeric",
12
+ month: "2-digit",
13
+ day: "2-digit",
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ hour12: false,
17
+ });
18
+
19
+ const formatLocalDateTime = (timestampMs: number): string => {
20
+ const date = new Date(timestampMs);
21
+ const parts = LOCAL_DATE_TIME_FORMATTER.formatToParts(date);
22
+ const get = (type: Intl.DateTimeFormatPartTypes) =>
23
+ parts.find((part) => part.type === type)?.value;
24
+ const year = get("year");
25
+ const month = get("month");
26
+ const day = get("day");
27
+ const hour = get("hour");
28
+ const minute = get("minute");
29
+ if (!year || !month || !day || !hour || !minute) {
30
+ return LOCAL_DATE_TIME_FORMATTER.format(date);
31
+ }
32
+ return `${year}-${month}-${day} ${hour}:${minute}`;
33
+ };
34
+
10
35
  describe("branchFormatter", () => {
11
36
  describe("buildLastToolUsageLabel with version", () => {
12
37
  const baseBranch: BranchInfo = {
@@ -31,7 +56,11 @@ describe("branchFormatter", () => {
31
56
  };
32
57
 
33
58
  const item = formatBranchItem(branch);
34
- expect(item.lastToolUsageLabel).toBe("Claude@1.0.3 | 2024-01-08 12:00");
59
+ expect(item.lastToolUsageLabel).toBe(
60
+ `Claude@1.0.3 | ${formatLocalDateTime(
61
+ new Date("2024-01-08T12:00:00").getTime(),
62
+ )}`,
63
+ );
35
64
  });
36
65
 
37
66
  it("should format as 'ToolName@latest | 2024-01-08 12:00' when version is null", () => {
@@ -48,7 +77,11 @@ describe("branchFormatter", () => {
48
77
  };
49
78
 
50
79
  const item = formatBranchItem(branch);
51
- expect(item.lastToolUsageLabel).toBe("Claude@latest | 2024-01-08 12:00");
80
+ expect(item.lastToolUsageLabel).toBe(
81
+ `Claude@latest | ${formatLocalDateTime(
82
+ new Date("2024-01-08T12:00:00").getTime(),
83
+ )}`,
84
+ );
52
85
  });
53
86
 
54
87
  it("should format as 'ToolName@latest | 2024-01-08 12:00' when version is undefined", () => {
@@ -64,7 +97,11 @@ describe("branchFormatter", () => {
64
97
  };
65
98
 
66
99
  const item = formatBranchItem(branch);
67
- expect(item.lastToolUsageLabel).toBe("Claude@latest | 2024-01-08 12:00");
100
+ expect(item.lastToolUsageLabel).toBe(
101
+ `Claude@latest | ${formatLocalDateTime(
102
+ new Date("2024-01-08T12:00:00").getTime(),
103
+ )}`,
104
+ );
68
105
  });
69
106
 
70
107
  it("should format Codex with version", () => {
@@ -82,7 +119,9 @@ describe("branchFormatter", () => {
82
119
 
83
120
  const item = formatBranchItem(branch);
84
121
  expect(item.lastToolUsageLabel).toBe(
85
- "Codex@2.1.0-beta.1 | 2024-01-08 15:30",
122
+ `Codex@2.1.0-beta.1 | ${formatLocalDateTime(
123
+ new Date("2024-01-08T15:30:00").getTime(),
124
+ )}`,
86
125
  );
87
126
  });
88
127
 
@@ -100,7 +139,11 @@ describe("branchFormatter", () => {
100
139
  };
101
140
 
102
141
  const item = formatBranchItem(branch);
103
- expect(item.lastToolUsageLabel).toBe("Gemini@0.5.0 | 2024-01-08 09:15");
142
+ expect(item.lastToolUsageLabel).toBe(
143
+ `Gemini@0.5.0 | ${formatLocalDateTime(
144
+ new Date("2024-01-08T09:15:00").getTime(),
145
+ )}`,
146
+ );
104
147
  });
105
148
 
106
149
  it("should format custom tool with version", () => {
@@ -117,7 +160,11 @@ describe("branchFormatter", () => {
117
160
  };
118
161
 
119
162
  const item = formatBranchItem(branch);
120
- expect(item.lastToolUsageLabel).toBe("MyTool@1.0.0 | 2024-01-08 10:00");
163
+ expect(item.lastToolUsageLabel).toBe(
164
+ `MyTool@1.0.0 | ${formatLocalDateTime(
165
+ new Date("2024-01-08T10:00:00").getTime(),
166
+ )}`,
167
+ );
121
168
  });
122
169
 
123
170
  it("should return null when lastToolUsage is undefined", () => {
@@ -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
+ });
@@ -17,14 +17,42 @@ function mapToolLabel(toolId: string, toolLabel?: string): string {
17
17
  return "Custom";
18
18
  }
19
19
 
20
+ const LOCAL_DATE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
21
+ year: "numeric",
22
+ month: "2-digit",
23
+ day: "2-digit",
24
+ hour: "2-digit",
25
+ minute: "2-digit",
26
+ hour12: false,
27
+ });
28
+
29
+ const formatLocalDateTimeParts = (date: Date): string => {
30
+ const parts = LOCAL_DATE_TIME_FORMATTER.formatToParts(date);
31
+ const get = (type: Intl.DateTimeFormatPartTypes) =>
32
+ parts.find((part) => part.type === type)?.value;
33
+ const year = get("year");
34
+ const month = get("month");
35
+ const day = get("day");
36
+ const hour = get("hour");
37
+ const minute = get("minute");
38
+
39
+ if (!year || !month || !day || !hour || !minute) {
40
+ return LOCAL_DATE_TIME_FORMATTER.format(date);
41
+ }
42
+
43
+ return `${year}-${month}-${day} ${hour}:${minute}`;
44
+ };
45
+
46
+ export function formatLocalDateTime(timestampMs: number): string {
47
+ const date = new Date(timestampMs);
48
+ if (Number.isNaN(date.getTime())) {
49
+ return "";
50
+ }
51
+ return formatLocalDateTimeParts(date);
52
+ }
53
+
20
54
  function formatTimestamp(ts: number): string {
21
- const date = new Date(ts);
22
- const year = date.getFullYear();
23
- const month = String(date.getMonth() + 1).padStart(2, "0");
24
- const day = String(date.getDate()).padStart(2, "0");
25
- const hours = String(date.getHours()).padStart(2, "0");
26
- const minutes = String(date.getMinutes()).padStart(2, "0");
27
- return `${year}-${month}-${day} ${hours}:${minutes}`;
55
+ return formatLocalDateTime(ts);
28
56
  }
29
57
 
30
58
  function buildLastToolUsageLabel(
@@ -1,9 +1,15 @@
1
1
  import type { SessionData, ToolSessionEntry } from "../../../config/index.js";
2
+ import type { SessionSearchOptions } from "../../../utils/session/index.js";
2
3
  import {
4
+ findLatestClaudeSession,
3
5
  findLatestClaudeSessionId,
6
+ findLatestCodexSession,
4
7
  findLatestCodexSessionId,
8
+ findLatestGeminiSession,
5
9
  findLatestGeminiSessionId,
6
- } from "../../../utils/session.js";
10
+ findLatestOpenCodeSession,
11
+ } from "../../../utils/session/index.js";
12
+ import { listAllWorktrees } from "../../../worktree.js";
7
13
 
8
14
  export interface ContinueSessionContext {
9
15
  history: ToolSessionEntry[];
@@ -122,10 +128,10 @@ export function findLatestBranchSessionsByTool(
122
128
  const byBranch = history.filter((entry) => entry && entry.branch === branch);
123
129
  if (!byBranch.length) return [];
124
130
 
125
- const scoped = worktreePath
131
+ const source = worktreePath
126
132
  ? byBranch.filter((entry) => entry.worktreePath === worktreePath)
127
133
  : byBranch;
128
- const source = scoped.length ? scoped : byBranch;
134
+ if (!source.length) return [];
129
135
 
130
136
  const latestByTool = new Map<string, ToolSessionEntry>();
131
137
  for (const entry of source) {
@@ -142,3 +148,84 @@ export function findLatestBranchSessionsByTool(
142
148
  (a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0),
143
149
  );
144
150
  }
151
+
152
+ export interface QuickStartRefreshContext {
153
+ branch: string;
154
+ worktreePath?: string | null;
155
+ }
156
+
157
+ export interface QuickStartSessionLookups {
158
+ findLatestCodexSession?: typeof findLatestCodexSession;
159
+ findLatestClaudeSession?: typeof findLatestClaudeSession;
160
+ findLatestGeminiSession?: typeof findLatestGeminiSession;
161
+ findLatestOpenCodeSession?: typeof findLatestOpenCodeSession;
162
+ listAllWorktrees?: typeof listAllWorktrees;
163
+ }
164
+
165
+ export async function refreshQuickStartEntries(
166
+ entries: ToolSessionEntry[],
167
+ context: QuickStartRefreshContext,
168
+ lookups: QuickStartSessionLookups = {},
169
+ ): Promise<ToolSessionEntry[]> {
170
+ if (!entries.length) return entries;
171
+ const worktreePath = context.worktreePath ?? null;
172
+ if (!worktreePath) return entries;
173
+
174
+ const lookupWorktrees = lookups.listAllWorktrees ?? listAllWorktrees;
175
+ let resolvedWorktrees: { path: string; branch: string }[] | null = null;
176
+ try {
177
+ const allWorktrees = await lookupWorktrees();
178
+ resolvedWorktrees = allWorktrees
179
+ .filter((entry) => entry?.path && entry?.branch)
180
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
181
+ } catch {
182
+ resolvedWorktrees = null;
183
+ }
184
+
185
+ const searchOptions: SessionSearchOptions = {
186
+ branch: context.branch,
187
+ ...(resolvedWorktrees && resolvedWorktrees.length > 0
188
+ ? { worktrees: resolvedWorktrees }
189
+ : {}),
190
+ };
191
+
192
+ const lookupCodex = lookups.findLatestCodexSession ?? findLatestCodexSession;
193
+ const lookupClaude =
194
+ lookups.findLatestClaudeSession ?? findLatestClaudeSession;
195
+ const lookupGemini =
196
+ lookups.findLatestGeminiSession ?? findLatestGeminiSession;
197
+ const lookupOpenCode =
198
+ lookups.findLatestOpenCodeSession ?? findLatestOpenCodeSession;
199
+
200
+ const updated = await Promise.all(
201
+ entries.map(async (entry) => {
202
+ let latest: { id: string; mtime: number } | null = null;
203
+ switch (entry.toolId) {
204
+ case "codex-cli":
205
+ latest = await lookupCodex(searchOptions);
206
+ break;
207
+ case "claude-code":
208
+ latest = await lookupClaude(worktreePath, searchOptions);
209
+ break;
210
+ case "gemini-cli":
211
+ latest = await lookupGemini(searchOptions);
212
+ break;
213
+ case "opencode":
214
+ latest = await lookupOpenCode(searchOptions);
215
+ break;
216
+ default:
217
+ return entry;
218
+ }
219
+
220
+ if (!latest?.id) return entry;
221
+ const updatedTimestamp = Math.max(entry.timestamp ?? 0, latest.mtime);
222
+ return {
223
+ ...entry,
224
+ sessionId: latest.id,
225
+ timestamp: updatedTimestamp,
226
+ };
227
+ }),
228
+ );
229
+
230
+ return updated.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
231
+ }
@@ -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";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Version cache for coding agents (FR-028 ~ FR-031)
3
+ *
4
+ * Provides caching mechanism for npm package versions to avoid
5
+ * repeated API calls during wizard navigation.
6
+ */
7
+
8
+ import type { VersionInfo } from "../../../utils/npmRegistry.js";
9
+
10
+ /**
11
+ * In-memory cache for agent versions
12
+ * Maps agentId -> VersionInfo[]
13
+ */
14
+ const versionCache = new Map<string, VersionInfo[]>();
15
+
16
+ /**
17
+ * Type for version fetch function (injectable for testing)
18
+ */
19
+ export type VersionFetcher = (agentId: string) => Promise<VersionInfo[]>;
20
+
21
+ /**
22
+ * Get cached versions for an agent
23
+ * @returns Cached versions or null if not cached
24
+ */
25
+ export function getVersionCache(agentId: string): VersionInfo[] | null {
26
+ const cached = versionCache.get(agentId);
27
+ return cached !== undefined ? cached : null;
28
+ }
29
+
30
+ /**
31
+ * Check if versions are cached for an agent
32
+ */
33
+ export function isVersionCachePopulated(agentId: string): boolean {
34
+ return versionCache.has(agentId);
35
+ }
36
+
37
+ /**
38
+ * Clear all cached versions
39
+ */
40
+ export function clearVersionCache(): void {
41
+ versionCache.clear();
42
+ }
43
+
44
+ /**
45
+ * Prefetch versions for multiple agents in parallel
46
+ * This should be called at application startup
47
+ *
48
+ * @param agentIds - List of agent IDs to prefetch versions for
49
+ * @param fetchFn - Optional custom fetch function (for testing)
50
+ */
51
+ export async function prefetchAgentVersions(
52
+ agentIds: string[],
53
+ fetchFn?: VersionFetcher,
54
+ ): Promise<void> {
55
+ // Import default fetcher if not provided
56
+ const fetcher =
57
+ fetchFn ??
58
+ (async (agentId: string) => {
59
+ const { fetchVersionOptionsForAgent } =
60
+ await import("./versionFetcher.js");
61
+ return fetchVersionOptionsForAgent(agentId);
62
+ });
63
+
64
+ // Fetch all versions in parallel
65
+ const results = await Promise.allSettled(
66
+ agentIds.map(async (agentId) => {
67
+ try {
68
+ const versions = await fetcher(agentId);
69
+ versionCache.set(agentId, versions);
70
+ } catch {
71
+ // On error, don't set cache (getVersionCache will return null)
72
+ }
73
+ }),
74
+ );
75
+
76
+ // Log any failures for debugging (silently handled)
77
+ for (let i = 0; i < results.length; i++) {
78
+ const result = results[i];
79
+ if (result && result.status === "rejected") {
80
+ // Silently handle - cache will return null for this agent
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Set versions in cache (for testing or direct population)
87
+ */
88
+ export function setVersionCache(
89
+ agentId: string,
90
+ versions: VersionInfo[],
91
+ ): void {
92
+ versionCache.set(agentId, versions);
93
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Version fetcher for coding agents
3
+ *
4
+ * Provides functions to fetch version information from npm registry
5
+ * and local installations.
6
+ */
7
+
8
+ import type { VersionInfo } from "../../../utils/npmRegistry.js";
9
+ import {
10
+ fetchPackageVersions,
11
+ parsePackageCommand,
12
+ } from "../../../utils/npmRegistry.js";
13
+ import { BUILTIN_CODING_AGENTS } from "../../../config/builtin-coding-agents.js";
14
+ import { findCommand } from "../../../utils/command.js";
15
+ import type { SelectInputItem } from "../components/solid/SelectInput.js";
16
+
17
+ /**
18
+ * Get package name for an agent ID (only for bunx-type agents)
19
+ */
20
+ export function getPackageNameForAgent(agentId: string): string | null {
21
+ const agent = BUILTIN_CODING_AGENTS.find((a) => a.id === agentId);
22
+ if (!agent || agent.type !== "bunx") {
23
+ return null;
24
+ }
25
+ const { packageName } = parsePackageCommand(agent.command);
26
+ return packageName;
27
+ }
28
+
29
+ /**
30
+ * Fetch version options for an agent from npm registry
31
+ */
32
+ export async function fetchVersionOptionsForAgent(
33
+ agentId: string,
34
+ ): Promise<VersionInfo[]> {
35
+ const packageName = getPackageNameForAgent(agentId);
36
+ if (!packageName) {
37
+ return [];
38
+ }
39
+ return fetchPackageVersions(packageName);
40
+ }
41
+
42
+ /**
43
+ * Agent ID to command name mapping
44
+ */
45
+ const AGENT_COMMAND_MAP: Record<string, string> = {
46
+ "claude-code": "claude",
47
+ "codex-cli": "codex",
48
+ "gemini-cli": "gemini",
49
+ opencode: "opencode",
50
+ };
51
+
52
+ /**
53
+ * Installed version information
54
+ */
55
+ export interface InstalledVersionInfo {
56
+ version: string;
57
+ path: string;
58
+ }
59
+
60
+ /**
61
+ * Fetch installed version information for an agent
62
+ * Returns null if not installed locally
63
+ */
64
+ export async function fetchInstalledVersionForAgent(
65
+ agentId: string,
66
+ ): Promise<InstalledVersionInfo | null> {
67
+ const commandName = AGENT_COMMAND_MAP[agentId];
68
+ if (!commandName) {
69
+ return null;
70
+ }
71
+
72
+ const result = await findCommand(commandName);
73
+ if (result.source !== "installed" || !result.path) {
74
+ return null;
75
+ }
76
+
77
+ // Version format: v1.0.3 -> 1.0.3
78
+ const version = result.version?.replace(/^v/, "") ?? "unknown";
79
+
80
+ return {
81
+ version,
82
+ path: result.path,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Convert VersionInfo to SelectInputItem for UI
88
+ */
89
+ export function versionInfoToSelectItem(v: VersionInfo): SelectInputItem {
90
+ const item: SelectInputItem = {
91
+ label: v.isPrerelease ? `${v.version} (pre)` : v.version,
92
+ value: v.version,
93
+ };
94
+ if (v.publishedAt) {
95
+ item.description = new Date(v.publishedAt).toLocaleDateString();
96
+ }
97
+ return item;
98
+ }
99
+
100
+ /**
101
+ * Create installed option for SelectInput
102
+ */
103
+ export function createInstalledOption(
104
+ installed: InstalledVersionInfo,
105
+ ): SelectInputItem {
106
+ return {
107
+ label: `installed@${installed.version}`,
108
+ value: "installed",
109
+ description: installed.path,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Get all bunx-type agent IDs
115
+ */
116
+ export function getBunxAgentIds(): string[] {
117
+ return BUILTIN_CODING_AGENTS.filter((a) => a.type === "bunx").map(
118
+ (a) => a.id,
119
+ );
120
+ }