@akiojin/gwt 2.11.0 → 2.12.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 (76) hide show
  1. package/dist/claude.d.ts +4 -1
  2. package/dist/claude.d.ts.map +1 -1
  3. package/dist/claude.js +51 -7
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.d.ts +7 -0
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +307 -18
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts +21 -0
  10. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -0
  11. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +145 -0
  12. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -0
  13. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -1
  14. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  15. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +4 -2
  16. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  17. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +1 -1
  18. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +10 -2
  19. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts.map +1 -1
  20. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +18 -7
  21. package/dist/cli/ui/components/screens/SessionSelectorScreen.js.map +1 -1
  22. package/dist/cli/ui/types.d.ts +1 -1
  23. package/dist/cli/ui/types.d.ts.map +1 -1
  24. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  25. package/dist/cli/ui/utils/branchFormatter.js +0 -13
  26. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  27. package/dist/cli/ui/utils/continueSession.d.ts +18 -0
  28. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
  29. package/dist/cli/ui/utils/continueSession.js +67 -0
  30. package/dist/cli/ui/utils/continueSession.js.map +1 -0
  31. package/dist/codex.d.ts +4 -1
  32. package/dist/codex.d.ts.map +1 -1
  33. package/dist/codex.js +70 -5
  34. package/dist/codex.js.map +1 -1
  35. package/dist/config/index.d.ts +9 -1
  36. package/dist/config/index.d.ts.map +1 -1
  37. package/dist/config/index.js +11 -2
  38. package/dist/config/index.js.map +1 -1
  39. package/dist/gemini.d.ts +4 -1
  40. package/dist/gemini.d.ts.map +1 -1
  41. package/dist/gemini.js +146 -32
  42. package/dist/gemini.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +118 -8
  45. package/dist/index.js.map +1 -1
  46. package/dist/qwen.d.ts +4 -1
  47. package/dist/qwen.d.ts.map +1 -1
  48. package/dist/qwen.js +45 -4
  49. package/dist/qwen.js.map +1 -1
  50. package/dist/utils/session.d.ts +82 -0
  51. package/dist/utils/session.d.ts.map +1 -0
  52. package/dist/utils/session.js +579 -0
  53. package/dist/utils/session.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/claude.ts +69 -8
  56. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
  57. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +2 -2
  58. package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +142 -0
  59. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +14 -0
  60. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +29 -10
  61. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +4 -1
  62. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +0 -1
  63. package/src/cli/ui/components/App.tsx +403 -23
  64. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
  65. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
  66. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
  67. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
  68. package/src/cli/ui/types.ts +1 -0
  69. package/src/cli/ui/utils/branchFormatter.ts +0 -13
  70. package/src/cli/ui/utils/continueSession.ts +106 -0
  71. package/src/codex.ts +91 -6
  72. package/src/config/index.ts +22 -2
  73. package/src/gemini.ts +179 -41
  74. package/src/index.ts +144 -16
  75. package/src/qwen.ts +56 -5
  76. package/src/utils/session.ts +704 -0
@@ -0,0 +1,237 @@
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, type SelectItem } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
+
8
+ export type QuickStartAction = "reuse-continue" | "reuse-new" | "manual";
9
+
10
+ export interface BranchQuickStartOption {
11
+ toolId?: string | null;
12
+ toolLabel: string;
13
+ toolCategory?: "Codex" | "Claude" | "Gemini" | "Qwen" | "Other";
14
+ model?: string | null;
15
+ sessionId?: string | null;
16
+ inferenceLevel?: string | null;
17
+ skipPermissions?: boolean | null;
18
+ }
19
+
20
+ const REASONING_LABELS: Record<string, string> = {
21
+ low: "Low",
22
+ medium: "Medium",
23
+ high: "High",
24
+ xhigh: "Extra high",
25
+ };
26
+
27
+ const formatReasoning = (level?: string | null) =>
28
+ level ? REASONING_LABELS[level] ?? level : "Default";
29
+
30
+ const formatSkip = (skip?: boolean | null) =>
31
+ skip === true ? "Yes" : skip === false ? "No" : "No";
32
+
33
+ const supportsReasoning = (toolId?: string | null) =>
34
+ toolId === "codex-cli";
35
+
36
+ const describe = (opt: BranchQuickStartOption, includeSessionId = true) => {
37
+ const parts = [`Model: ${opt.model ?? "default"}`];
38
+ if (supportsReasoning(opt.toolId)) {
39
+ parts.push(`Reasoning: ${formatReasoning(opt.inferenceLevel)}`);
40
+ }
41
+ parts.push(`Skip: ${formatSkip(opt.skipPermissions)}`);
42
+ if (includeSessionId) {
43
+ parts.push(opt.sessionId ? `ID: ${opt.sessionId}` : "No ID");
44
+ }
45
+ return parts.join(" / ");
46
+ };
47
+
48
+ type QuickStartItem = SelectItem & {
49
+ description: string;
50
+ disabled?: boolean;
51
+ toolId?: string | null;
52
+ action: QuickStartAction;
53
+ groupStart?: boolean;
54
+ category: string;
55
+ categoryColor: "cyan" | "yellow" | "magenta" | "green" | "white";
56
+ };
57
+
58
+ export interface BranchQuickStartScreenProps {
59
+ previousOptions: BranchQuickStartOption[];
60
+ loading?: boolean;
61
+ onBack: () => void;
62
+ onSelect: (action: QuickStartAction, toolId?: string | null) => void;
63
+ version?: string | null;
64
+ branchName: string;
65
+ }
66
+
67
+ export function BranchQuickStartScreen({
68
+ previousOptions,
69
+ loading = false,
70
+ onBack,
71
+ onSelect,
72
+ version,
73
+ branchName,
74
+ }: BranchQuickStartScreenProps) {
75
+ const { rows } = useTerminalSize();
76
+ const containerHeight = rows && rows > 0 ? rows : undefined;
77
+
78
+ const CATEGORY_META = {
79
+ "codex-cli": { label: "Codex", color: "cyan" },
80
+ "claude-code": { label: "Claude", color: "yellow" },
81
+ "gemini-cli": { label: "Gemini", color: "magenta" },
82
+ "qwen-cli": { label: "Qwen", color: "green" },
83
+ other: { label: "Other", color: "white" },
84
+ } as const;
85
+
86
+ type CategoryMeta = (typeof CATEGORY_META)[keyof typeof CATEGORY_META];
87
+
88
+ const resolveCategory = (toolId?: string | null): CategoryMeta => {
89
+ switch (toolId) {
90
+ case "codex-cli":
91
+ return CATEGORY_META["codex-cli"];
92
+ case "claude-code":
93
+ return CATEGORY_META["claude-code"];
94
+ case "gemini-cli":
95
+ return CATEGORY_META["gemini-cli"];
96
+ case "qwen-cli":
97
+ return CATEGORY_META["qwen-cli"];
98
+ default:
99
+ return CATEGORY_META.other;
100
+ }
101
+ };
102
+
103
+ const items: QuickStartItem[] = previousOptions.length
104
+ ? (() => {
105
+ const order = ["Claude", "Codex", "Gemini", "Qwen", "Other"];
106
+ const sorted = [...previousOptions].sort((a, b) => {
107
+ const ca = resolveCategory(a.toolId).label;
108
+ const cb = resolveCategory(b.toolId).label;
109
+ return order.indexOf(ca) - order.indexOf(cb);
110
+ });
111
+
112
+ const flat: QuickStartItem[] = [];
113
+ sorted.forEach((opt, idx) => {
114
+ const cat = resolveCategory(opt.toolId);
115
+ const prevCat =
116
+ idx > 0 ? resolveCategory(sorted[idx - 1]?.toolId).label : null;
117
+ const isNewCategory = prevCat !== cat.label;
118
+
119
+ flat.push(
120
+ {
121
+ label: "Resume",
122
+ value: `reuse-continue:${opt.toolId ?? "unknown"}:${idx}`,
123
+ action: "reuse-continue",
124
+ toolId: opt.toolId ?? null,
125
+ description: describe(opt, true),
126
+ groupStart: isNewCategory && flat.length > 0,
127
+ category: cat.label,
128
+ categoryColor: cat.color,
129
+ },
130
+ {
131
+ label: "New",
132
+ value: `reuse-new:${opt.toolId ?? "unknown"}:${idx}`,
133
+ action: "reuse-new",
134
+ toolId: opt.toolId ?? null,
135
+ description: describe(opt, false),
136
+ groupStart: false,
137
+ category: cat.label,
138
+ categoryColor: cat.color,
139
+ },
140
+ );
141
+ });
142
+
143
+ return flat;
144
+ })()
145
+ : [
146
+ {
147
+ label: "Resume with previous settings",
148
+ value: "reuse-continue",
149
+ action: "reuse-continue",
150
+ description: "No previous settings (disabled)",
151
+ disabled: true,
152
+ category: CATEGORY_META.other.label,
153
+ categoryColor: CATEGORY_META.other.color,
154
+ },
155
+ {
156
+ label: "Start new with previous settings",
157
+ value: "reuse-new",
158
+ action: "reuse-new",
159
+ description: "No previous settings (disabled)",
160
+ disabled: true,
161
+ category: CATEGORY_META.other.label,
162
+ categoryColor: CATEGORY_META.other.color,
163
+ },
164
+ ];
165
+
166
+ items.push({
167
+ label: "Manual selection",
168
+ value: "manual",
169
+ action: "manual",
170
+ description: "Pick tool and model manually",
171
+ category: CATEGORY_META.other.label,
172
+ categoryColor: CATEGORY_META.other.color,
173
+ });
174
+
175
+ useInput((_, key) => {
176
+ if (key.escape) {
177
+ onBack();
178
+ }
179
+ });
180
+
181
+ return (
182
+ <Box flexDirection="column" height={containerHeight}>
183
+ <Header
184
+ title="Quick Start"
185
+ titleColor="cyan"
186
+ version={version}
187
+ />
188
+
189
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
190
+ <Box marginBottom={1} flexDirection="column">
191
+ <Text>
192
+ {loading
193
+ ? "Loading previous settings..."
194
+ : "Resume with previous settings, start new, or choose manually."}
195
+ </Text>
196
+ <Text color="gray">{`Branch: ${branchName}`}</Text>
197
+ </Box>
198
+ <Select
199
+ items={items}
200
+ onSelect={(item: QuickStartItem) => {
201
+ if (item.disabled) return;
202
+ onSelect(item.action, item.toolId ?? null);
203
+ }}
204
+ renderItem={(item: QuickStartItem, isSelected) => (
205
+ <Box
206
+ flexDirection="column"
207
+ marginTop={item.groupStart ? 1 : item.category === "Other" ? 1 : 0}
208
+ >
209
+ <Text>
210
+ <Text
211
+ color={item.categoryColor}
212
+ inverse={isSelected}
213
+ >
214
+ {`[${item.category}] `}
215
+ </Text>
216
+ <Text inverse={isSelected}>
217
+ {item.label}
218
+ {item.disabled ? " (disabled)" : ""}
219
+ </Text>
220
+ </Text>
221
+ {item.description && (
222
+ <Text color="gray"> {item.description}</Text>
223
+ )}
224
+ </Box>
225
+ )}
226
+ />
227
+ </Box>
228
+
229
+ <Footer
230
+ actions={[
231
+ { key: "enter", description: "Select" },
232
+ { key: "esc", description: "Back" },
233
+ ]}
234
+ />
235
+ </Box>
236
+ );
237
+ }
@@ -28,6 +28,7 @@ export interface ExecutionModeSelectorScreenProps {
28
28
  onBack: () => void;
29
29
  onSelect: (result: ExecutionModeResult) => void;
30
30
  version?: string | null;
31
+ continueSessionId?: string | null;
31
32
  }
32
33
 
33
34
  /**
@@ -40,6 +41,7 @@ export function ExecutionModeSelectorScreen({
40
41
  onBack,
41
42
  onSelect,
42
43
  version,
44
+ continueSessionId = null,
43
45
  }: ExecutionModeSelectorScreenProps) {
44
46
  const { rows } = useTerminalSize();
45
47
  const [step, setStep] = useState<1 | 2>(1);
@@ -67,7 +69,9 @@ export function ExecutionModeSelectorScreen({
67
69
  description: "Start fresh session",
68
70
  },
69
71
  {
70
- label: "Continue",
72
+ label: continueSessionId
73
+ ? `Continue (ID: ${continueSessionId})`
74
+ : "Continue",
71
75
  value: "continue",
72
76
  description: "Continue from last session",
73
77
  },
@@ -230,7 +230,7 @@ export function ModelSelectorScreen({
230
230
  return (
231
231
  <Box flexDirection="column" height={rows}>
232
232
  <Header
233
- title={step === "model" ? "Model Selection" : "Inference Level"}
233
+ title={step === "model" ? "Model Selection" : "Reasoning Level"}
234
234
  titleColor="blue"
235
235
  version={version}
236
236
  />
@@ -8,10 +8,18 @@ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
8
8
  export interface SessionItem {
9
9
  label: string;
10
10
  value: string;
11
+ secondary?: string;
11
12
  }
12
13
 
13
14
  export interface SessionSelectorScreenProps {
14
- sessions: string[];
15
+ sessions: {
16
+ sessionId: string;
17
+ branch: string;
18
+ toolLabel?: string | null;
19
+ timestamp?: number;
20
+ }[];
21
+ loading?: boolean;
22
+ errorMessage?: string | null;
15
23
  onBack: () => void;
16
24
  onSelect: (session: string) => void;
17
25
  version?: string | null;
@@ -23,6 +31,8 @@ export interface SessionSelectorScreenProps {
23
31
  */
24
32
  export function SessionSelectorScreen({
25
33
  sessions,
34
+ loading = false,
35
+ errorMessage = null,
26
36
  onBack,
27
37
  onSelect,
28
38
  version,
@@ -38,10 +48,18 @@ export function SessionSelectorScreen({
38
48
  });
39
49
 
40
50
  // Format sessions for Select component
41
- const sessionItems: SessionItem[] = sessions.map((session) => ({
42
- label: session,
43
- value: session,
44
- }));
51
+ const sessionItems: SessionItem[] = sessions.map((session) => {
52
+ const startedAt =
53
+ typeof session.timestamp === "number"
54
+ ? new Date(session.timestamp).toLocaleString()
55
+ : "unknown time";
56
+ const toolLabel = session.toolLabel ?? "unknown";
57
+ const label = `${session.branch} â€ĸ ${toolLabel} â€ĸ ${session.sessionId} (${startedAt})`;
58
+ return {
59
+ label,
60
+ value: session.sessionId,
61
+ };
62
+ });
45
63
 
46
64
  // Handle session selection
47
65
  const handleSelect = (item: SessionItem) => {
@@ -84,13 +102,23 @@ export function SessionSelectorScreen({
84
102
 
85
103
  {/* Content */}
86
104
  <Box flexDirection="column" flexGrow={1}>
87
- {sessions.length === 0 ? (
105
+ {loading ? (
106
+ <Box>
107
+ <Text dimColor>Loading sessions...</Text>
108
+ </Box>
109
+ ) : sessions.length === 0 ? (
88
110
  <Box>
89
111
  <Text dimColor>No sessions found</Text>
90
112
  </Box>
91
113
  ) : (
92
114
  <Select items={sessionItems} onSelect={handleSelect} limit={limit} />
93
115
  )}
116
+
117
+ {errorMessage ? (
118
+ <Box marginTop={1}>
119
+ <Text color="red">{errorMessage}</Text>
120
+ </Box>
121
+ ) : null}
94
122
  </Box>
95
123
 
96
124
  {/* Footer */}
@@ -184,6 +184,7 @@ export type ScreenType =
184
184
  | "branch-list"
185
185
  | "branch-creator"
186
186
  | "branch-action-selector"
187
+ | "branch-quick-start"
187
188
  | "ai-tool-selector"
188
189
  | "model-selector"
189
190
  | "session-selector"
@@ -99,15 +99,6 @@ function mapToolLabel(toolId: string, toolLabel?: string): string {
99
99
  return "Custom";
100
100
  }
101
101
 
102
- function mapModeLabel(
103
- mode?: "normal" | "continue" | "resume" | null,
104
- ): string | null {
105
- if (mode === "normal") return "New";
106
- if (mode === "continue") return "Continue";
107
- if (mode === "resume") return "Resume";
108
- return null;
109
- }
110
-
111
102
  function formatTimestamp(ts: number): string {
112
103
  const date = new Date(ts);
113
104
  const year = date.getFullYear();
@@ -123,12 +114,8 @@ function buildLastToolUsageLabel(
123
114
  ): string | null {
124
115
  if (!usage) return null;
125
116
  const toolText = mapToolLabel(usage.toolId, usage.toolLabel);
126
- const modeText = mapModeLabel(usage.mode);
127
117
  const timestamp = usage.timestamp ? formatTimestamp(usage.timestamp) : null;
128
118
  const parts = [toolText];
129
- if (modeText) {
130
- parts.push(modeText);
131
- }
132
119
  if (timestamp) {
133
120
  parts.push(timestamp);
134
121
  }
@@ -0,0 +1,106 @@
1
+ import type { SessionData, ToolSessionEntry } from "../../../config/index.js";
2
+
3
+ export interface ContinueSessionContext {
4
+ history: ToolSessionEntry[];
5
+ sessionData: SessionData | null;
6
+ branch: string;
7
+ toolId: string;
8
+ repoRoot: string | null;
9
+ }
10
+
11
+ /**
12
+ * æŒ‡åŽšã•ã‚ŒãŸãƒ–ãƒŠãƒŗãƒ/ツãƒŧãƒĢãĢᴐãĨく最新ã‚ģãƒƒã‚ˇãƒ§ãƒŗIDã‚’č§Ŗæąēする。
13
+ * 1. åąĨæ­´(history)ぎ最新マッチをå„Ē先
14
+ * 2. lastSessionId ãŒãƒ–ãƒŠãƒŗãƒ/ツãƒŧãƒĢä¸€č‡´ã§ã‚ã‚Œã°åˆŠį”¨
15
+ * 3. ãã‚Œã§ã‚‚į„Ąã„å ´åˆã€åŒä¸€ãƒ–ãƒŠãƒŗãƒ/ツãƒŧãƒĢであればツãƒŧãƒĢå›ē有ぎäŋå­˜å ´æ‰€ã‹ã‚‰æ¤œå‡ē
16
+ */
17
+ export async function resolveContinueSessionId(
18
+ context: ContinueSessionContext,
19
+ ): Promise<string | null> {
20
+ const {
21
+ history,
22
+ sessionData,
23
+ branch,
24
+ toolId,
25
+ repoRoot,
26
+ } = context;
27
+
28
+ // 1) åąĨ歴から最新マッチをæŽĸすīŧˆæœĢå°žã‹ã‚‰éĄã‚‹īŧ‰
29
+ for (let i = history.length - 1; i >= 0; i -= 1) {
30
+ const entry = history[i];
31
+ if (
32
+ entry &&
33
+ entry.branch === branch &&
34
+ entry.toolId === toolId &&
35
+ entry.sessionId
36
+ ) {
37
+ return entry.sessionId;
38
+ }
39
+ }
40
+
41
+ // 2) lastSessionId ãŒä¸€č‡´ã™ã‚‹å ´åˆã¯ãã‚Œã‚’čŋ”す
42
+ if (
43
+ sessionData?.lastSessionId &&
44
+ sessionData.lastBranch === branch &&
45
+ sessionData.lastUsedTool === toolId
46
+ ) {
47
+ return sessionData.lastSessionId;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ export function findLatestBranchSession(
54
+ history: ToolSessionEntry[],
55
+ branch: string,
56
+ toolId?: string | null,
57
+ ): ToolSessionEntry | null {
58
+ const byBranch = history.filter((entry) => entry && entry.branch === branch);
59
+ if (!byBranch.length) return null;
60
+
61
+ const pickLatest = (entries: ToolSessionEntry[]) =>
62
+ entries.reduce<ToolSessionEntry | null>((latest, entry) => {
63
+ if (!latest) return entry;
64
+ const latestTs = latest.timestamp ?? 0;
65
+ const currentTs = entry.timestamp ?? 0;
66
+ return currentTs >= latestTs ? entry : latest;
67
+ }, null);
68
+
69
+ if (toolId) {
70
+ const byTool = byBranch.filter((entry) => entry.toolId === toolId);
71
+ if (byTool.length) {
72
+ return pickLatest(byTool);
73
+ }
74
+ }
75
+
76
+ return pickLatest(byBranch);
77
+ }
78
+
79
+ export function findLatestBranchSessionsByTool(
80
+ history: ToolSessionEntry[],
81
+ branch: string,
82
+ worktreePath?: string | null,
83
+ ): ToolSessionEntry[] {
84
+ const byBranch = history.filter((entry) => entry && entry.branch === branch);
85
+ if (!byBranch.length) return [];
86
+
87
+ const scoped = worktreePath
88
+ ? byBranch.filter((entry) => entry.worktreePath === worktreePath)
89
+ : byBranch;
90
+ const source = scoped.length ? scoped : byBranch;
91
+
92
+ const latestByTool = new Map<string, ToolSessionEntry>();
93
+ for (const entry of source) {
94
+ if (!entry.toolId) continue;
95
+ const current = latestByTool.get(entry.toolId);
96
+ const currentTs = current?.timestamp ?? 0;
97
+ const entryTs = entry.timestamp ?? 0;
98
+ if (!current || entryTs >= currentTs) {
99
+ latestByTool.set(entry.toolId, entry);
100
+ }
101
+ }
102
+
103
+ return Array.from(latestByTool.values()).sort(
104
+ (a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0),
105
+ );
106
+ }
package/src/codex.ts CHANGED
@@ -3,6 +3,10 @@ import chalk from "chalk";
3
3
  import { platform } from "os";
4
4
  import { existsSync } from "fs";
5
5
  import { createChildStdio, getTerminalStreams } from "./utils/terminal.js";
6
+ import {
7
+ findLatestCodexSession,
8
+ waitForCodexSessionId,
9
+ } from "./utils/session.js";
6
10
 
7
11
  const CODEX_CLI_PACKAGE = "@openai/codex@latest";
8
12
 
@@ -17,6 +21,8 @@ export const buildDefaultCodexArgs = (
17
21
  ): string[] => [
18
22
  "--enable",
19
23
  "web_search_request",
24
+ "--enable",
25
+ "skills",
20
26
  `--model=${model}`,
21
27
  "--sandbox",
22
28
  "workspace-write",
@@ -53,9 +59,11 @@ export async function launchCodexCLI(
53
59
  envOverrides?: Record<string, string>;
54
60
  model?: string;
55
61
  reasoningEffort?: CodexReasoningEffort;
62
+ sessionId?: string | null;
56
63
  } = {},
57
- ): Promise<void> {
64
+ ): Promise<{ sessionId?: string | null }> {
58
65
  const terminal = getTerminalStreams();
66
+ const startedAt = Date.now();
59
67
 
60
68
  try {
61
69
  if (!existsSync(worktreePath)) {
@@ -73,14 +81,40 @@ export async function launchCodexCLI(
73
81
  console.log(chalk.green(` đŸŽ¯ Model: ${model}`));
74
82
  console.log(chalk.green(` 🧠 Reasoning: ${reasoningEffort}`));
75
83
 
84
+ const resumeSessionId =
85
+ options.sessionId && options.sessionId.trim().length > 0
86
+ ? options.sessionId.trim()
87
+ : null;
88
+
89
+ // Start polling session files immediately to catch the session created right after launch.
90
+ const sessionProbe = waitForCodexSessionId({ startedAt, cwd: worktreePath }).catch(
91
+ () => null,
92
+ );
93
+
76
94
  switch (options.mode) {
77
95
  case "continue":
78
- args.push("resume", "--last");
79
- console.log(chalk.cyan(" â­ī¸ Resuming last Codex session"));
96
+ if (resumeSessionId) {
97
+ args.push("resume", resumeSessionId);
98
+ console.log(
99
+ chalk.cyan(
100
+ ` â­ī¸ Resuming specific Codex session: ${resumeSessionId}`,
101
+ ),
102
+ );
103
+ } else {
104
+ args.push("resume", "--last");
105
+ console.log(chalk.cyan(" â­ī¸ Resuming last Codex session"));
106
+ }
80
107
  break;
81
108
  case "resume":
82
- args.push("resume");
83
- console.log(chalk.cyan(" 🔄 Resume command"));
109
+ if (resumeSessionId) {
110
+ args.push("resume", resumeSessionId);
111
+ console.log(
112
+ chalk.cyan(` 🔄 Resuming Codex session: ${resumeSessionId}`),
113
+ );
114
+ } else {
115
+ args.push("resume");
116
+ console.log(chalk.cyan(" 🔄 Resume command"));
117
+ }
84
118
  break;
85
119
  case "normal":
86
120
  default:
@@ -101,6 +135,8 @@ export async function launchCodexCLI(
101
135
 
102
136
  args.push(...codexArgs);
103
137
 
138
+ console.log(chalk.gray(` 📋 Args: ${args.join(" ")}`));
139
+
104
140
  terminal.exitRawMode();
105
141
 
106
142
  const childStdio = createChildStdio();
@@ -108,16 +144,63 @@ export async function launchCodexCLI(
108
144
  const env = { ...process.env, ...(options.envOverrides ?? {}) };
109
145
 
110
146
  try {
111
- await execa("bunx", [CODEX_CLI_PACKAGE, ...args], {
147
+ const execChild = async (child: any) => {
148
+ try {
149
+ await child;
150
+ } catch (execError: any) {
151
+ // Treat SIGINT/SIGTERM as normal exit (user pressed Ctrl+C)
152
+ if (execError.signal === "SIGINT" || execError.signal === "SIGTERM") {
153
+ return;
154
+ }
155
+ throw execError;
156
+ }
157
+ };
158
+
159
+ const child = execa("bunx", [CODEX_CLI_PACKAGE, ...args], {
112
160
  cwd: worktreePath,
161
+ shell: true,
113
162
  stdin: childStdio.stdin,
114
163
  stdout: childStdio.stdout,
115
164
  stderr: childStdio.stderr,
116
165
  env,
117
166
  } as any);
167
+ await execChild(child);
118
168
  } finally {
119
169
  childStdio.cleanup();
120
170
  }
171
+
172
+ // File-based session detection only - no stdout capture
173
+ // Use only findLatestCodexSession with short timeout, skip sessionProbe to avoid hanging
174
+ let capturedSessionId: string | null = null;
175
+ const finishedAt = Date.now();
176
+ try {
177
+ const latest = await findLatestCodexSession({
178
+ since: startedAt,
179
+ until: finishedAt + 30_000,
180
+ preferClosestTo: finishedAt,
181
+ windowMs: 10 * 60 * 1000,
182
+ cwd: worktreePath,
183
+ });
184
+ // Priority: latest on disk > resumeSessionId
185
+ capturedSessionId = latest?.id ?? resumeSessionId ?? null;
186
+ } catch {
187
+ capturedSessionId = resumeSessionId ?? null;
188
+ }
189
+
190
+ if (capturedSessionId) {
191
+ console.log(chalk.cyan(`\n 🆔 Session ID: ${capturedSessionId}`));
192
+ console.log(
193
+ chalk.gray(` Resume command: codex resume ${capturedSessionId}`),
194
+ );
195
+ } else {
196
+ console.log(
197
+ chalk.yellow(
198
+ "\n â„šī¸ Could not determine Codex session ID automatically.",
199
+ ),
200
+ );
201
+ }
202
+
203
+ return capturedSessionId ? { sessionId: capturedSessionId } : {};
121
204
  } catch (error: any) {
122
205
  const errorMessage =
123
206
  error.code === "ENOENT"
@@ -142,6 +225,8 @@ export async function launchCodexCLI(
142
225
  }
143
226
 
144
227
  throw new CodexError(errorMessage, error);
228
+ } finally {
229
+ terminal.exitRawMode();
145
230
  }
146
231
  }
147
232