@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.
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/continueSession.d.ts +18 -0
  25. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
  26. package/dist/cli/ui/utils/continueSession.js +67 -0
  27. package/dist/cli/ui/utils/continueSession.js.map +1 -0
  28. package/dist/codex.d.ts +4 -1
  29. package/dist/codex.d.ts.map +1 -1
  30. package/dist/codex.js +70 -5
  31. package/dist/codex.js.map +1 -1
  32. package/dist/config/index.d.ts +9 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/index.js +11 -2
  35. package/dist/config/index.js.map +1 -1
  36. package/dist/gemini.d.ts +4 -1
  37. package/dist/gemini.d.ts.map +1 -1
  38. package/dist/gemini.js +146 -32
  39. package/dist/gemini.js.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +119 -48
  42. package/dist/index.js.map +1 -1
  43. package/dist/qwen.d.ts +4 -1
  44. package/dist/qwen.d.ts.map +1 -1
  45. package/dist/qwen.js +45 -4
  46. package/dist/qwen.js.map +1 -1
  47. package/dist/utils/prompt.d.ts +6 -0
  48. package/dist/utils/prompt.d.ts.map +1 -0
  49. package/dist/utils/prompt.js +57 -0
  50. package/dist/utils/prompt.js.map +1 -0
  51. package/dist/utils/session.d.ts +82 -0
  52. package/dist/utils/session.d.ts.map +1 -0
  53. package/dist/utils/session.js +579 -0
  54. package/dist/utils/session.js.map +1 -0
  55. package/package.json +2 -2
  56. package/src/claude.ts +69 -8
  57. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -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/components/App.tsx +403 -23
  63. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
  64. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
  65. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
  66. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
  67. package/src/cli/ui/types.ts +1 -0
  68. package/src/cli/ui/utils/continueSession.ts +106 -0
  69. package/src/codex.ts +91 -6
  70. package/src/config/index.ts +22 -2
  71. package/src/gemini.ts +179 -41
  72. package/src/index.ts +145 -61
  73. package/src/qwen.ts +56 -5
  74. package/src/utils/__tests__/prompt.test.ts +89 -0
  75. package/src/utils/prompt.ts +74 -0
  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"
@@ -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
 
@@ -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(sessionData: SessionData): Promise<void> {
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 (sessionData.lastBranch && sessionData.lastWorktreePath) {
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
  }