@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.
- package/dist/claude.d.ts +4 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +51 -7
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts +7 -0
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +307 -18
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts +21 -0
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +145 -0
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -1
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +4 -2
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js +1 -1
- package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +10 -2
- package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/SessionSelectorScreen.js +18 -7
- package/dist/cli/ui/components/screens/SessionSelectorScreen.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +0 -13
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/continueSession.d.ts +18 -0
- package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
- package/dist/cli/ui/utils/continueSession.js +67 -0
- package/dist/cli/ui/utils/continueSession.js.map +1 -0
- package/dist/codex.d.ts +4 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +70 -5
- package/dist/codex.js.map +1 -1
- package/dist/config/index.d.ts +9 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -2
- package/dist/config/index.js.map +1 -1
- package/dist/gemini.d.ts +4 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +146 -32
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +118 -8
- package/dist/index.js.map +1 -1
- package/dist/qwen.d.ts +4 -1
- package/dist/qwen.d.ts.map +1 -1
- package/dist/qwen.js +45 -4
- package/dist/qwen.js.map +1 -1
- package/dist/utils/session.d.ts +82 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +579 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +1 -1
- package/src/claude.ts +69 -8
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +2 -2
- package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +142 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +14 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +29 -10
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +4 -1
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +0 -1
- package/src/cli/ui/components/App.tsx +403 -23
- package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
- package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
- package/src/cli/ui/types.ts +1 -0
- package/src/cli/ui/utils/branchFormatter.ts +0 -13
- package/src/cli/ui/utils/continueSession.ts +106 -0
- package/src/codex.ts +91 -6
- package/src/config/index.ts +22 -2
- package/src/gemini.ts +179 -41
- package/src/index.ts +144 -16
- package/src/qwen.ts +56 -5
- 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:
|
|
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" : "
|
|
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:
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
{
|
|
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 */}
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|