@akiojin/gwt 2.11.1 → 2.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.
- 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/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 +119 -48
- 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/prompt.d.ts +6 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +57 -0
- package/dist/utils/prompt.js.map +1 -0
- 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 +2 -2
- 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/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/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/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 +145 -61
- package/src/qwen.ts +56 -5
- package/src/utils/__tests__/prompt.test.ts +89 -0
- package/src/utils/prompt.ts +74 -0
- 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
|
@@ -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
|
|
package/src/config/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface SessionData {
|
|
|
14
14
|
lastWorktreePath: string | null;
|
|
15
15
|
lastBranch: string | null;
|
|
16
16
|
lastUsedTool?: string;
|
|
17
|
+
lastSessionId?: string | null;
|
|
18
|
+
reasoningLevel?: string | null;
|
|
19
|
+
skipPermissions?: boolean | null;
|
|
17
20
|
timestamp: number;
|
|
18
21
|
repositoryRoot: string;
|
|
19
22
|
mode?: "normal" | "continue" | "resume";
|
|
@@ -27,8 +30,11 @@ export interface ToolSessionEntry {
|
|
|
27
30
|
worktreePath: string | null;
|
|
28
31
|
toolId: string;
|
|
29
32
|
toolLabel: string;
|
|
33
|
+
sessionId?: string | null;
|
|
30
34
|
mode?: "normal" | "continue" | "resume" | null;
|
|
31
35
|
model?: string | null;
|
|
36
|
+
reasoningLevel?: string | null;
|
|
37
|
+
skipPermissions?: boolean | null;
|
|
32
38
|
timestamp: number;
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -110,7 +116,10 @@ function getSessionFilePath(repositoryRoot: string): string {
|
|
|
110
116
|
return path.join(sessionDir, `${repoName}_${repoHash}.json`);
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
export async function saveSession(
|
|
119
|
+
export async function saveSession(
|
|
120
|
+
sessionData: SessionData,
|
|
121
|
+
options: { skipHistory?: boolean } = {},
|
|
122
|
+
): Promise<void> {
|
|
114
123
|
try {
|
|
115
124
|
const sessionPath = getSessionFilePath(sessionData.repositoryRoot);
|
|
116
125
|
const sessionDir = path.dirname(sessionPath);
|
|
@@ -131,15 +140,22 @@ export async function saveSession(sessionData: SessionData): Promise<void> {
|
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
// 新しい履歴エントリを追加(branch/worktree/toolが揃っている場合のみ)
|
|
134
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
!options.skipHistory &&
|
|
145
|
+
sessionData.lastBranch &&
|
|
146
|
+
sessionData.lastWorktreePath
|
|
147
|
+
) {
|
|
135
148
|
const entry: ToolSessionEntry = {
|
|
136
149
|
branch: sessionData.lastBranch,
|
|
137
150
|
worktreePath: sessionData.lastWorktreePath,
|
|
138
151
|
toolId: sessionData.lastUsedTool ?? "unknown",
|
|
139
152
|
toolLabel:
|
|
140
153
|
sessionData.toolLabel ?? sessionData.lastUsedTool ?? "Custom",
|
|
154
|
+
sessionId: sessionData.lastSessionId ?? null,
|
|
141
155
|
mode: sessionData.mode ?? null,
|
|
142
156
|
model: sessionData.model ?? null,
|
|
157
|
+
reasoningLevel: sessionData.reasoningLevel ?? null,
|
|
158
|
+
skipPermissions: sessionData.skipPermissions ?? false,
|
|
143
159
|
timestamp: sessionData.timestamp,
|
|
144
160
|
};
|
|
145
161
|
existingHistory = [...existingHistory, entry].slice(-100); // keep latest 100
|
|
@@ -148,6 +164,9 @@ export async function saveSession(sessionData: SessionData): Promise<void> {
|
|
|
148
164
|
const payload: SessionData = {
|
|
149
165
|
...sessionData,
|
|
150
166
|
history: existingHistory,
|
|
167
|
+
lastSessionId: sessionData.lastSessionId ?? null,
|
|
168
|
+
reasoningLevel: sessionData.reasoningLevel ?? null,
|
|
169
|
+
skipPermissions: sessionData.skipPermissions ?? false,
|
|
151
170
|
};
|
|
152
171
|
|
|
153
172
|
await writeFile(sessionPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
@@ -264,6 +283,7 @@ export async function getLastToolUsageMap(
|
|
|
264
283
|
toolLabel: parsed.toolLabel ?? parsed.lastUsedTool ?? "Custom",
|
|
265
284
|
mode: parsed.mode ?? null,
|
|
266
285
|
model: parsed.model ?? null,
|
|
286
|
+
reasoningLevel: parsed.reasoningLevel ?? null,
|
|
267
287
|
timestamp: parsed.timestamp ?? Date.now(),
|
|
268
288
|
});
|
|
269
289
|
}
|