@juspay/shooter 1.15.0 → 1.16.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/build/client/_app/immutable/assets/{0.BZLcOr5z.css → 0.DEfoFaGR.css} +1 -1
- package/build/client/_app/immutable/assets/0.DEfoFaGR.css.br +0 -0
- package/build/client/_app/immutable/assets/0.DEfoFaGR.css.gz +0 -0
- package/build/client/_app/immutable/chunks/Bkqjn62J.js +1 -0
- package/build/client/_app/immutable/chunks/Bkqjn62J.js.br +1 -0
- package/build/client/_app/immutable/chunks/Bkqjn62J.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{gxvWeAns.js → DOHhmtDH.js} +1 -1
- package/build/client/_app/immutable/chunks/DOHhmtDH.js.br +0 -0
- package/build/client/_app/immutable/chunks/DOHhmtDH.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{pMo6RVvN.js → DlS3abGJ.js} +1 -1
- package/build/client/_app/immutable/chunks/DlS3abGJ.js.br +0 -0
- package/build/client/_app/immutable/chunks/DlS3abGJ.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.B0PrrcUG.js → app.CSJG7N9H.js} +2 -2
- package/build/client/_app/immutable/entry/app.CSJG7N9H.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CSJG7N9H.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.CTt1901T.js +1 -0
- package/build/client/_app/immutable/entry/start.CTt1901T.js.br +2 -0
- package/build/client/_app/immutable/entry/start.CTt1901T.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.D4GLHqPM.js → 0.qOL7xtFn.js} +1 -1
- package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.nJde5z5O.js → 1.Di708Ago.js} +1 -1
- package/build/client/_app/immutable/nodes/1.Di708Ago.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Di708Ago.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.CLtsjLeG.js → 2.DSM1znqa.js} +1 -1
- package/build/client/_app/immutable/nodes/2.DSM1znqa.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DSM1znqa.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.CKTUHtnx.js → 3.BPa5fh75.js} +1 -1
- package/build/client/_app/immutable/nodes/3.BPa5fh75.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BPa5fh75.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js +1 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js +4 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js +2 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js +2 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-BUSWGJr9.js → 0-D8uPamNd.js} +3 -3
- package/build/server/chunks/{0-BUSWGJr9.js.map → 0-D8uPamNd.js.map} +1 -1
- package/build/server/chunks/{1-DjiQE1K0.js → 1-DhtioHbs.js} +2 -2
- package/build/server/chunks/{1-DjiQE1K0.js.map → 1-DhtioHbs.js.map} +1 -1
- package/build/server/chunks/{2-ThgVrRKa.js → 2-Cgh7ZFgE.js} +2 -2
- package/build/server/chunks/{2-ThgVrRKa.js.map → 2-Cgh7ZFgE.js.map} +1 -1
- package/build/server/chunks/{3-G5LiDFQ9.js → 3-I6hnjssH.js} +2 -2
- package/build/server/chunks/{3-G5LiDFQ9.js.map → 3-I6hnjssH.js.map} +1 -1
- package/build/server/chunks/{6--I7fF3Bx.js → 6-bPDbH1_W.js} +2 -2
- package/build/server/chunks/{6--I7fF3Bx.js.map → 6-bPDbH1_W.js.map} +1 -1
- package/build/server/chunks/{7-BwPLVwOR.js → 7-CpBrOkxQ.js} +2 -2
- package/build/server/chunks/{7-BwPLVwOR.js.map → 7-CpBrOkxQ.js.map} +1 -1
- package/build/server/chunks/{8-BwOMHaoQ.js → 8-BRGAVfze.js} +2 -2
- package/build/server/chunks/{8-BwOMHaoQ.js.map → 8-BRGAVfze.js.map} +1 -1
- package/build/server/chunks/{9-DkO6aJIB.js → 9-C6xuAb_Y.js} +2 -2
- package/build/server/chunks/{9-DkO6aJIB.js.map → 9-C6xuAb_Y.js.map} +1 -1
- package/build/server/chunks/{_server.ts-ByPExYfO.js → _server.ts-BLNDdFWC.js} +3 -3
- package/build/server/chunks/_server.ts-BLNDdFWC.js.map +1 -0
- package/build/server/chunks/{_server.ts-40c_epk8.js → _server.ts-BrRZXr-8.js} +5 -4
- package/build/server/chunks/_server.ts-BrRZXr-8.js.map +1 -0
- package/build/server/chunks/{_server.ts-BuYyCrnF.js → _server.ts-C6xbNz6d.js} +4 -3
- package/build/server/chunks/_server.ts-C6xbNz6d.js.map +1 -0
- package/build/server/chunks/{_server.ts-CjpQ10xh.js → _server.ts-CjK0g9dO.js} +18 -2
- package/build/server/chunks/_server.ts-CjK0g9dO.js.map +1 -0
- package/build/server/chunks/{_server.ts-0Xr2fWaq.js → _server.ts-Cq9_scaV.js} +17 -7
- package/build/server/chunks/_server.ts-Cq9_scaV.js.map +1 -0
- package/build/server/chunks/{_server.ts-CilRds58.js → _server.ts-D--_NXt2.js} +25 -16
- package/build/server/chunks/_server.ts-D--_NXt2.js.map +1 -0
- package/build/server/chunks/{_server.ts-2ixC-X3K.js → _server.ts-Dekgb6Hx.js} +4 -3
- package/build/server/chunks/_server.ts-Dekgb6Hx.js.map +1 -0
- package/build/server/chunks/opencode-db-path-CRgzBK5U.js +402 -0
- package/build/server/chunks/opencode-db-path-CRgzBK5U.js.map +1 -0
- package/build/server/chunks/{pty-manager-TyMUpDA9.js → pty-manager-aFpChJah.js} +35 -2
- package/build/server/chunks/pty-manager-aFpChJah.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +16 -16
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/server.ts +12 -0
- package/src/lib/modules/client/common/index.ts +1 -0
- package/src/lib/modules/client/common/provider.ts +30 -0
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +2 -0
- package/src/lib/modules/server/sessions/codex-parser.ts +286 -0
- package/src/lib/modules/server/sessions/codex-reader.ts +293 -0
- package/src/lib/modules/server/sessions/process-detector.ts +23 -0
- package/src/lib/modules/server/terminal/codex-watcher.ts +179 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +41 -0
- package/src/lib/theme.css +21 -1
- package/src/lib/types/codex.ts +21 -0
- package/src/lib/types/generated/Sessions.ts +5 -1
- package/src/lib/types/index.ts +1 -0
- package/src/lib/types/server.ts +18 -5
- package/src/lib/types/sessions.ts +1 -1
- package/src/routes/api/device-token/+server.ts +7 -3
- package/src/routes/api/sessions/+server.ts +27 -15
- package/src/routes/api/sessions/connect/+server.ts +19 -11
- package/src/routes/api/terminals/+server.ts +1 -1
- package/src/routes/project/+page.svelte +7 -23
- package/src/routes/session/[id]/+page.svelte +3 -3
- package/src/routes/terminals/+page.svelte +1 -1
- package/src/routes/terminals/[id]/+page.svelte +1 -1
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.br +0 -0
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.gz +0 -0
- package/build/client/_app/immutable/chunks/X-tVU_3P.js +0 -1
- package/build/client/_app/immutable/chunks/X-tVU_3P.js.br +0 -0
- package/build/client/_app/immutable/chunks/X-tVU_3P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/gxvWeAns.js.br +0 -0
- package/build/client/_app/immutable/chunks/gxvWeAns.js.gz +0 -0
- package/build/client/_app/immutable/chunks/pMo6RVvN.js.br +0 -0
- package/build/client/_app/immutable/chunks/pMo6RVvN.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.B0PrrcUG.js.br +0 -0
- package/build/client/_app/immutable/entry/app.B0PrrcUG.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.B1obDjVk.js +0 -1
- package/build/client/_app/immutable/entry/start.B1obDjVk.js.br +0 -0
- package/build/client/_app/immutable/entry/start.B1obDjVk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.nJde5z5O.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.nJde5z5O.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js +0 -1
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js +0 -4
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js +0 -2
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js +0 -2
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js.gz +0 -0
- package/build/server/chunks/_server.ts-0Xr2fWaq.js.map +0 -1
- package/build/server/chunks/_server.ts-2ixC-X3K.js.map +0 -1
- package/build/server/chunks/_server.ts-40c_epk8.js.map +0 -1
- package/build/server/chunks/_server.ts-BuYyCrnF.js.map +0 -1
- package/build/server/chunks/_server.ts-ByPExYfO.js.map +0 -1
- package/build/server/chunks/_server.ts-CilRds58.js.map +0 -1
- package/build/server/chunks/_server.ts-CjpQ10xh.js.map +0 -1
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js +0 -15
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js.map +0 -1
- package/build/server/chunks/pty-manager-TyMUpDA9.js.map +0 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI session reader — listing + conversation retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors jsonl-reader.ts (Claude) and opencode-reader.ts (OpenCode): produces
|
|
5
|
+
* the same SessionInfo / ProjectGroup / ConversationMessage shapes so the rest
|
|
6
|
+
* of the app is provider-agnostic.
|
|
7
|
+
*
|
|
8
|
+
* Codex sessions live at ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
|
|
9
|
+
* (and ~/.codex/archived_sessions/). These files can be very large (hundreds of
|
|
10
|
+
* MB), so listing only reads a bounded prefix of each file for metadata/title
|
|
11
|
+
* and estimates the message count from file size; the conversation reader bounds
|
|
12
|
+
* its read for oversized files.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ConversationMessage, ProjectGroup, SessionInfo } from '$lib/types';
|
|
16
|
+
|
|
17
|
+
import * as crypto from 'crypto';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import { homedir } from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
|
|
22
|
+
import { parseCodexMeta, parseCodexRollout } from './codex-parser';
|
|
23
|
+
|
|
24
|
+
/** Bytes read from the head of a rollout file when listing (enough for meta + first prompts). */
|
|
25
|
+
const LIST_PREFIX_BYTES = 256 * 1024;
|
|
26
|
+
/** Above this size, the conversation reader reads only the tail to bound memory. */
|
|
27
|
+
const MAX_FULL_READ_BYTES = 16 * 1024 * 1024;
|
|
28
|
+
/** Rough average bytes per Codex message line, used to estimate message counts cheaply. */
|
|
29
|
+
const APPROX_BYTES_PER_MESSAGE = 3000;
|
|
30
|
+
|
|
31
|
+
/** User-message wrappers that Codex injects automatically — not real prompts. */
|
|
32
|
+
const SYNTHETIC_PROMPT_PREFIXES = ['<environment_context>', '<user_instructions>', '<permissions'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find Codex sessions whose rollout file was written within `thresholdMs` —
|
|
36
|
+
* i.e. sessions that are currently (or very recently) active. Uses filesystem
|
|
37
|
+
* mtime rather than ~/.codex/state_5.sqlite, which is WAL-locked while Codex runs.
|
|
38
|
+
*/
|
|
39
|
+
export function detectActiveCodexSessions(
|
|
40
|
+
thresholdMs: number
|
|
41
|
+
): { cwd: string; id: string; startedAt: number }[] {
|
|
42
|
+
const cutoff = Date.now() - thresholdMs;
|
|
43
|
+
const out: { cwd: string; id: string; startedAt: number }[] = [];
|
|
44
|
+
for (const filePath of collectRolloutFiles()) {
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(filePath);
|
|
47
|
+
if (stat.mtimeMs < cutoff) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const meta = parseCodexMeta(readPrefix(filePath).split('\n')[0] ?? '');
|
|
51
|
+
if (meta) {
|
|
52
|
+
out.push({ cwd: meta.cwd, id: meta.id, startedAt: stat.birthtimeMs });
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// skip unreadable files
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find the rollout file for a Codex session launched in `cwd` after `sinceMs`
|
|
63
|
+
* (used by pty-manager to link a freshly-launched `codex` terminal to its file).
|
|
64
|
+
* Picks the newest matching file by creation time.
|
|
65
|
+
*/
|
|
66
|
+
export function findCodexRolloutForCwd(cwd: string, sinceMs: number): null | string {
|
|
67
|
+
let best: null | { birthtime: number; path: string } = null;
|
|
68
|
+
for (const filePath of collectRolloutFiles()) {
|
|
69
|
+
try {
|
|
70
|
+
const stat = fs.statSync(filePath);
|
|
71
|
+
if (stat.birthtimeMs <= sinceMs) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const meta = parseCodexMeta(readPrefix(filePath).split('\n')[0] ?? '');
|
|
75
|
+
if (meta?.cwd === cwd && (!best || stat.birthtimeMs > best.birthtime)) {
|
|
76
|
+
best = { birthtime: stat.birthtimeMs, path: filePath };
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// skip unreadable files
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return best?.path ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return a page of a Codex session's conversation. With `offset` 0 the most
|
|
87
|
+
* recent `limit` messages are returned, backed up to a user-message boundary so
|
|
88
|
+
* turns aren't clipped; otherwise the `offset`..`offset + limit` slice is returned.
|
|
89
|
+
*/
|
|
90
|
+
export function getCodexConversation(
|
|
91
|
+
sessionId: string,
|
|
92
|
+
offset = 0,
|
|
93
|
+
limit = 200
|
|
94
|
+
): ConversationMessage[] {
|
|
95
|
+
const filePath = findRolloutPath(sessionId);
|
|
96
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { messages } = parseCodexRollout(readRolloutText(filePath));
|
|
102
|
+
|
|
103
|
+
// Match the Claude reader: with no explicit offset, return the most recent
|
|
104
|
+
// `limit` messages, backing up to a user-message boundary so turns aren't clipped.
|
|
105
|
+
if (offset === 0 && messages.length > limit) {
|
|
106
|
+
let startIdx = messages.length - limit;
|
|
107
|
+
while (startIdx > 0 && messages[startIdx].role !== 'user') {
|
|
108
|
+
startIdx--;
|
|
109
|
+
}
|
|
110
|
+
return messages.slice(startIdx);
|
|
111
|
+
}
|
|
112
|
+
return messages.slice(offset, offset + limit);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[codex] Failed to read conversation:', error);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** List all Codex sessions grouped by working directory, most-recently-modified first. */
|
|
120
|
+
export function listCodexProjects(): ProjectGroup[] {
|
|
121
|
+
const files = collectRolloutFiles();
|
|
122
|
+
const byCwd = new Map<string, SessionInfo[]>();
|
|
123
|
+
|
|
124
|
+
for (const filePath of files) {
|
|
125
|
+
let stat: fs.Stats;
|
|
126
|
+
try {
|
|
127
|
+
stat = fs.statSync(filePath);
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let prefix: string;
|
|
132
|
+
try {
|
|
133
|
+
prefix = readPrefix(filePath);
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const firstLine = prefix.slice(0, Math.max(0, prefix.indexOf('\n')) || prefix.length);
|
|
139
|
+
const meta = parseCodexMeta(firstLine);
|
|
140
|
+
if (!meta) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const session: SessionInfo = {
|
|
145
|
+
created: meta.startedAt || stat.birthtime.toISOString(),
|
|
146
|
+
gitBranch: '',
|
|
147
|
+
id: meta.id,
|
|
148
|
+
messageCount: Math.max(1, Math.round(stat.size / APPROX_BYTES_PER_MESSAGE)),
|
|
149
|
+
modified: stat.mtime.toISOString(),
|
|
150
|
+
projectPath: meta.cwd,
|
|
151
|
+
source: 'codex' as const,
|
|
152
|
+
summary: '',
|
|
153
|
+
title: cleanTitle(firstUserPrompt(prefix)),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const bucket = byCwd.get(meta.cwd);
|
|
157
|
+
if (bucket) {
|
|
158
|
+
bucket.push(session);
|
|
159
|
+
} else {
|
|
160
|
+
byCwd.set(meta.cwd, [session]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const projects: ProjectGroup[] = [];
|
|
165
|
+
for (const [cwd, sessions] of byCwd) {
|
|
166
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
167
|
+
const segments = cwd.split('/').filter(Boolean);
|
|
168
|
+
projects.push({
|
|
169
|
+
fullPath: cwd,
|
|
170
|
+
id: shortHash(cwd),
|
|
171
|
+
lastModified: sessions[0]?.modified ?? '',
|
|
172
|
+
name: segments.slice(-2).join('/'),
|
|
173
|
+
sessionCount: sessions.length,
|
|
174
|
+
sessions,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return projects.sort(
|
|
179
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function cleanTitle(prompt: string): string {
|
|
184
|
+
const firstLine = prompt.replace(/\s+/g, ' ').trim().split('\n')[0]?.trim() ?? '';
|
|
185
|
+
if (!firstLine) {
|
|
186
|
+
return 'Untitled Session';
|
|
187
|
+
}
|
|
188
|
+
return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function codexSessionsDirs(): string[] {
|
|
192
|
+
const home = homedir();
|
|
193
|
+
return [path.join(home, '.codex', 'sessions'), path.join(home, '.codex', 'archived_sessions')];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Recursively collect all rollout-*.jsonl file paths under the Codex session roots. */
|
|
197
|
+
function collectRolloutFiles(): string[] {
|
|
198
|
+
const out: string[] = [];
|
|
199
|
+
const walk = (dir: string): void => {
|
|
200
|
+
let entries: fs.Dirent[];
|
|
201
|
+
try {
|
|
202
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
203
|
+
} catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const full = path.join(dir, entry.name);
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
walk(full);
|
|
210
|
+
} else if (entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
|
|
211
|
+
out.push(full);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
for (const root of codexSessionsDirs()) {
|
|
216
|
+
walk(root);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Locate a rollout file by its session UUID (embedded in the filename and session_meta.id). */
|
|
222
|
+
function findRolloutPath(sessionId: string): null | string {
|
|
223
|
+
if (!/^[0-9a-f-]+$/i.test(sessionId)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const suffix = `-${sessionId}.jsonl`;
|
|
227
|
+
return collectRolloutFiles().find((p) => path.basename(p).endsWith(suffix)) ?? null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Pull the first genuine user prompt (skipping Codex's auto-injected wrappers) for a title. */
|
|
231
|
+
function firstUserPrompt(prefixText: string): string {
|
|
232
|
+
for (const line of prefixText.split('\n')) {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed || !trimmed.includes('"type":"message"') || !trimmed.includes('"role":"user"')) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const entry = JSON.parse(trimmed) as {
|
|
239
|
+
payload?: { content?: { text?: string; type?: string }[] };
|
|
240
|
+
};
|
|
241
|
+
const text = (entry.payload?.content ?? [])
|
|
242
|
+
.filter((c) => c.type === 'input_text' && typeof c.text === 'string')
|
|
243
|
+
.map((c) => c.text ?? '')
|
|
244
|
+
.join('\n')
|
|
245
|
+
.trim();
|
|
246
|
+
if (text && !SYNTHETIC_PROMPT_PREFIXES.some((p) => text.startsWith(p))) {
|
|
247
|
+
return text;
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Read the first `LIST_PREFIX_BYTES` of a file as UTF-8 (complete lines only). */
|
|
257
|
+
function readPrefix(filePath: string): string {
|
|
258
|
+
const fd = fs.openSync(filePath, 'r');
|
|
259
|
+
try {
|
|
260
|
+
const buf = Buffer.alloc(LIST_PREFIX_BYTES);
|
|
261
|
+
const bytesRead = fs.readSync(fd, buf, 0, LIST_PREFIX_BYTES, 0);
|
|
262
|
+
const text = buf.toString('utf-8', 0, bytesRead);
|
|
263
|
+
const lastNl = text.lastIndexOf('\n');
|
|
264
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
265
|
+
} finally {
|
|
266
|
+
fs.closeSync(fd);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Read a rollout file's text, bounded to the tail for oversized files. */
|
|
271
|
+
function readRolloutText(filePath: string): string {
|
|
272
|
+
const stat = fs.statSync(filePath);
|
|
273
|
+
if (stat.size <= MAX_FULL_READ_BYTES) {
|
|
274
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
275
|
+
}
|
|
276
|
+
// Oversized: keep session_meta (first line) + the tail (most recent messages).
|
|
277
|
+
const head = readPrefix(filePath).split('\n')[0] ?? '';
|
|
278
|
+
const fd = fs.openSync(filePath, 'r');
|
|
279
|
+
try {
|
|
280
|
+
const start = stat.size - MAX_FULL_READ_BYTES;
|
|
281
|
+
const buf = Buffer.alloc(MAX_FULL_READ_BYTES);
|
|
282
|
+
const bytesRead = fs.readSync(fd, buf, 0, MAX_FULL_READ_BYTES, start);
|
|
283
|
+
const tail = buf.toString('utf-8', 0, bytesRead);
|
|
284
|
+
// Drop the first (likely partial) line of the tail.
|
|
285
|
+
return `${head}\n${tail.slice(tail.indexOf('\n') + 1)}`;
|
|
286
|
+
} finally {
|
|
287
|
+
fs.closeSync(fd);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function shortHash(input: string): string {
|
|
292
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
293
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClaudeSessionFile, DetectedProcess } from '$lib/types';
|
|
2
2
|
|
|
3
|
+
import { detectActiveCodexSessions } from '$lib/modules/server/sessions/codex-reader';
|
|
3
4
|
import { resolveOpenCodeDbPath } from '$lib/modules/server/sessions/opencode-db-path';
|
|
4
5
|
import Database from 'better-sqlite3';
|
|
5
6
|
import { execSync } from 'child_process';
|
|
@@ -45,6 +46,9 @@ const CLAUDE_SESSIONS_DIR = join(homedir(), '.claude', 'sessions');
|
|
|
45
46
|
// OpenCode sessions updated within this window are considered "live"
|
|
46
47
|
const OPENCODE_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
47
48
|
|
|
49
|
+
// Codex rollout files written within this window are considered "live"
|
|
50
|
+
const CODEX_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
51
|
+
|
|
48
52
|
/**
|
|
49
53
|
* Scan ~/.claude/sessions/*.json to find running Claude Code processes,
|
|
50
54
|
* and query the OpenCode SQLite DB for recently active sessions.
|
|
@@ -137,6 +141,25 @@ export function detectRunningAISessions(): DetectedProcess[] {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
// --- Codex sessions ---
|
|
145
|
+
// Codex has no PID file; a rollout file written in the last few minutes
|
|
146
|
+
// indicates an active session. cwd/id come from its session_meta line.
|
|
147
|
+
try {
|
|
148
|
+
for (const s of detectActiveCodexSessions(CODEX_ACTIVE_THRESHOLD_MS)) {
|
|
149
|
+
results.push({
|
|
150
|
+
command: 'codex',
|
|
151
|
+
cwd: s.cwd,
|
|
152
|
+
kind: 'interactive',
|
|
153
|
+
pid: 0, // Codex doesn't expose a per-session PID
|
|
154
|
+
projectPath: cwdToProjectPath(s.cwd),
|
|
155
|
+
sessionId: s.id,
|
|
156
|
+
startedAt: s.startedAt,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// ~/.codex/sessions missing or unreadable — skip silently
|
|
161
|
+
}
|
|
162
|
+
|
|
140
163
|
// Sort by startedAt descending (most recent first)
|
|
141
164
|
results.sort((a, b) => b.startedAt - a.startedAt);
|
|
142
165
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex live session watcher.
|
|
3
|
+
*
|
|
4
|
+
* Codex rollout files are append-only JSONL (like Claude's), so this mirrors
|
|
5
|
+
* SessionWatcher: chokidar detects writes, only the newly-appended bytes are
|
|
6
|
+
* read, and lines are fed to a CodexStreamParser that emits messages as each
|
|
7
|
+
* conversation run completes. Because Codex's role-run grouping only flushes a
|
|
8
|
+
* run when the *next* run begins, an idle timer flushes the final open run when
|
|
9
|
+
* writing stops, so the last message isn't withheld.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ConversationMessage, CodexWatchState as WatchState } from '$lib/types';
|
|
13
|
+
|
|
14
|
+
import { watch as chokidarWatch } from 'chokidar';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
|
|
17
|
+
import { CodexStreamParser, parseCodexRollout } from '../sessions/codex-parser';
|
|
18
|
+
|
|
19
|
+
/** Flush the open run after this many ms without a write. */
|
|
20
|
+
const IDLE_FLUSH_MS = 1500;
|
|
21
|
+
|
|
22
|
+
class CodexWatcher {
|
|
23
|
+
private watched = new Map<string, WatchState>();
|
|
24
|
+
|
|
25
|
+
/** One-shot full-history read of a rollout file (bypasses the live watch). */
|
|
26
|
+
getHistory(filePath: string): ConversationMessage[] {
|
|
27
|
+
if (!fs.existsSync(filePath)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return parseCodexRollout(fs.readFileSync(filePath, 'utf-8')).messages;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(`[codex-watcher] Failed to read history for ${filePath}:`, error);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Detach one `callback` (closing the watcher only when none remain), or stop entirely if no callback is given. */
|
|
39
|
+
stop(filePath: string, callback?: (messages: ConversationMessage[]) => void): void {
|
|
40
|
+
const state = this.watched.get(filePath);
|
|
41
|
+
if (!state) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (callback) {
|
|
45
|
+
state.callbacks.delete(callback);
|
|
46
|
+
if (state.callbacks.size > 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (state.idleTimer) {
|
|
51
|
+
clearTimeout(state.idleTimer);
|
|
52
|
+
}
|
|
53
|
+
void state.watcher.close();
|
|
54
|
+
this.watched.delete(filePath);
|
|
55
|
+
console.log(`[codex-watcher] Stopped watching: ${filePath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stop watching every file. */
|
|
59
|
+
stopAll(): void {
|
|
60
|
+
for (const [filePath] of this.watched) {
|
|
61
|
+
this.stop(filePath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Watch a rollout file for appended messages; returns an unsubscribe function. */
|
|
66
|
+
subscribe(filePath: string, callback: (messages: ConversationMessage[]) => void): () => void {
|
|
67
|
+
const existing = this.watched.get(filePath);
|
|
68
|
+
if (existing) {
|
|
69
|
+
existing.callbacks.add(callback);
|
|
70
|
+
return () => {
|
|
71
|
+
this.stop(filePath, callback);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const watcher = chokidarWatch(filePath, {
|
|
76
|
+
awaitWriteFinish: { pollInterval: 100, stabilityThreshold: 200 },
|
|
77
|
+
ignoreInitial: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const state: WatchState = {
|
|
81
|
+
callbacks: new Set([callback]),
|
|
82
|
+
idleTimer: null,
|
|
83
|
+
lineBuffer: '',
|
|
84
|
+
offset: fs.existsSync(filePath) ? fs.statSync(filePath).size : 0,
|
|
85
|
+
parser: new CodexStreamParser(),
|
|
86
|
+
watcher,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
watcher.on('change', () => {
|
|
90
|
+
this.readNew(filePath);
|
|
91
|
+
});
|
|
92
|
+
watcher.on('add', () => {
|
|
93
|
+
this.readNew(filePath);
|
|
94
|
+
});
|
|
95
|
+
watcher.on('error', (err) => {
|
|
96
|
+
console.error(`[codex-watcher] watch error ${filePath}:`, err);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.watched.set(filePath, state);
|
|
100
|
+
console.log(`[codex-watcher] Watching: ${filePath} (offset ${state.offset})`);
|
|
101
|
+
return () => {
|
|
102
|
+
this.stop(filePath, callback);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private emit(state: WatchState, messages: ConversationMessage[]): void {
|
|
107
|
+
if (messages.length === 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const cb of state.callbacks) {
|
|
111
|
+
try {
|
|
112
|
+
cb(messages);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('[codex-watcher] callback error:', err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private readNew(filePath: string): void {
|
|
120
|
+
const state = this.watched.get(filePath);
|
|
121
|
+
if (!state) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let stat: fs.Stats;
|
|
126
|
+
try {
|
|
127
|
+
stat = fs.statSync(filePath);
|
|
128
|
+
} catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (stat.size < state.offset) {
|
|
132
|
+
// Truncated/rotated — reset.
|
|
133
|
+
state.offset = 0;
|
|
134
|
+
state.lineBuffer = '';
|
|
135
|
+
state.parser = new CodexStreamParser();
|
|
136
|
+
}
|
|
137
|
+
if (stat.size <= state.offset) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fd = fs.openSync(filePath, 'r');
|
|
142
|
+
try {
|
|
143
|
+
const buf = Buffer.alloc(stat.size - state.offset);
|
|
144
|
+
fs.readSync(fd, buf, 0, buf.length, state.offset);
|
|
145
|
+
state.offset = stat.size;
|
|
146
|
+
|
|
147
|
+
const combined = state.lineBuffer + buf.toString('utf-8');
|
|
148
|
+
const segments = combined.split('\n');
|
|
149
|
+
state.lineBuffer = combined.endsWith('\n') ? '' : (segments.pop() ?? '');
|
|
150
|
+
|
|
151
|
+
const emitted: ConversationMessage[] = [];
|
|
152
|
+
for (const line of segments) {
|
|
153
|
+
if (line.trim()) {
|
|
154
|
+
emitted.push(...state.parser.pushLine(line));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.emit(state, emitted);
|
|
158
|
+
} finally {
|
|
159
|
+
fs.closeSync(fd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Reset the idle timer; flush the final open run once writes stop.
|
|
163
|
+
if (state.idleTimer) {
|
|
164
|
+
clearTimeout(state.idleTimer);
|
|
165
|
+
}
|
|
166
|
+
state.idleTimer = setTimeout(() => {
|
|
167
|
+
const current = this.watched.get(filePath);
|
|
168
|
+
if (current) {
|
|
169
|
+
this.emit(current, current.parser.flushOpen());
|
|
170
|
+
}
|
|
171
|
+
}, IDLE_FLUSH_MS);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Single shared instance across module loaders (same pattern as the other watchers).
|
|
176
|
+
const CW_GLOBAL_KEY = '__shooter_codex_watcher';
|
|
177
|
+
export const codexWatcher: CodexWatcher =
|
|
178
|
+
((globalThis as Record<string, unknown>)[CW_GLOBAL_KEY] as CodexWatcher) || new CodexWatcher();
|
|
179
|
+
(globalThis as Record<string, unknown>)[CW_GLOBAL_KEY] = codexWatcher;
|
|
@@ -12,6 +12,7 @@ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs'
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
|
|
15
|
+
import { findCodexRolloutForCwd } from '../sessions/codex-reader';
|
|
15
16
|
import { broadcastEvent } from '../ws/server.js';
|
|
16
17
|
import { HolderClient } from './holder-client';
|
|
17
18
|
import { openCodeWatcher } from './opencode-watcher';
|
|
@@ -964,6 +965,46 @@ class PtyManager {
|
|
|
964
965
|
5 * 60 * 1000
|
|
965
966
|
);
|
|
966
967
|
}
|
|
968
|
+
|
|
969
|
+
// For Codex: poll ~/.codex/sessions for the rollout file created after
|
|
970
|
+
// launch whose session_meta.cwd matches. Codex reuses the sessionFile
|
|
971
|
+
// field (a JSONL path, like Claude); the WS adapter routes it to the
|
|
972
|
+
// Codex watcher by its /.codex/ path.
|
|
973
|
+
if (command === 'codex') {
|
|
974
|
+
const launchTime = terminal.createdAt.getTime();
|
|
975
|
+
terminal.pollTimer = setInterval(() => {
|
|
976
|
+
if (terminal.status === 'exited' || terminal.sessionFile) {
|
|
977
|
+
if (terminal.pollTimer) {
|
|
978
|
+
clearInterval(terminal.pollTimer);
|
|
979
|
+
terminal.pollTimer = null;
|
|
980
|
+
}
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
const rollout = findCodexRolloutForCwd(cwd, launchTime);
|
|
985
|
+
if (rollout) {
|
|
986
|
+
terminal.sessionFile = rollout;
|
|
987
|
+
if (terminal.pollTimer) {
|
|
988
|
+
clearInterval(terminal.pollTimer);
|
|
989
|
+
terminal.pollTimer = null;
|
|
990
|
+
}
|
|
991
|
+
terminalStore.update(id, { sessionFile: rollout });
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
// ignore filesystem errors
|
|
995
|
+
}
|
|
996
|
+
}, 1500);
|
|
997
|
+
|
|
998
|
+
setTimeout(
|
|
999
|
+
() => {
|
|
1000
|
+
if (terminal.pollTimer) {
|
|
1001
|
+
clearInterval(terminal.pollTimer);
|
|
1002
|
+
terminal.pollTimer = null;
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
5 * 60 * 1000
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
967
1008
|
}
|
|
968
1009
|
|
|
969
1010
|
/** Wire up all HolderClient callbacks (activity, CWD, output, exit, disconnect). */
|
package/src/lib/theme.css
CHANGED
|
@@ -486,7 +486,7 @@
|
|
|
486
486
|
--pill-cursor: inherit;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
.pill-source-claude {
|
|
489
|
+
.pill-source-claude-code {
|
|
490
490
|
--pill-background: var(--ds-blue-100);
|
|
491
491
|
--pill-color: var(--ds-blue-900);
|
|
492
492
|
--pill-font-size: 10px;
|
|
@@ -506,6 +506,26 @@
|
|
|
506
506
|
--pill-cursor: inherit;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
.pill-source-codex {
|
|
510
|
+
--pill-background: var(--ds-green-100);
|
|
511
|
+
--pill-color: var(--ds-green-900);
|
|
512
|
+
--pill-font-size: 10px;
|
|
513
|
+
--pill-padding: 2px 8px;
|
|
514
|
+
--pill-hover-background: var(--ds-green-100);
|
|
515
|
+
--pill-hover-color: var(--ds-green-900);
|
|
516
|
+
--pill-cursor: inherit;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.pill-source-gemini {
|
|
520
|
+
--pill-background: var(--ds-gray-100);
|
|
521
|
+
--pill-color: var(--ds-gray-900);
|
|
522
|
+
--pill-font-size: 10px;
|
|
523
|
+
--pill-padding: 2px 8px;
|
|
524
|
+
--pill-hover-background: var(--ds-gray-100);
|
|
525
|
+
--pill-hover-color: var(--ds-gray-900);
|
|
526
|
+
--pill-cursor: inherit;
|
|
527
|
+
}
|
|
528
|
+
|
|
509
529
|
/* ===== Icon Sizes ===== */
|
|
510
530
|
.icon-14 {
|
|
511
531
|
--icon-width: 14px;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Codex CLI session types (hand-written: the rollout record shapes are
|
|
2
|
+
// provider-specific and not worth round-tripping through the YAML codegen).
|
|
3
|
+
// Codex stores sessions as JSONL "rollout" files under
|
|
4
|
+
// ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl.
|
|
5
|
+
|
|
6
|
+
import type { ConversationMessage } from './sessions';
|
|
7
|
+
|
|
8
|
+
/** Result of parsing a Codex rollout file into the canonical message model. */
|
|
9
|
+
export interface CodexParseResult {
|
|
10
|
+
messages: ConversationMessage[];
|
|
11
|
+
meta: CodexSessionMeta | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** First-line `session_meta` payload of a Codex rollout file. */
|
|
15
|
+
export interface CodexSessionMeta {
|
|
16
|
+
cliVersion: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
id: string;
|
|
19
|
+
model: string;
|
|
20
|
+
startedAt: string;
|
|
21
|
+
}
|
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
* @type { SessionSource }
|
|
15
15
|
* @description Source tool that produced the session
|
|
16
16
|
*/
|
|
17
|
-
export type SessionSource = 'claude-code' | 'opencode';
|
|
17
|
+
export type SessionSource = 'claude-code' | 'opencode' | 'codex' | 'gemini';
|
|
18
18
|
|
|
19
19
|
export function decodeSessionSource(rawInput: unknown): SessionSource | null {
|
|
20
20
|
switch (rawInput) {
|
|
21
21
|
case 'claude-code':
|
|
22
22
|
case 'opencode':
|
|
23
|
+
case 'codex':
|
|
24
|
+
case 'gemini':
|
|
23
25
|
return rawInput;
|
|
24
26
|
}
|
|
25
27
|
return null;
|
|
@@ -29,6 +31,8 @@ export function _decodeSessionSource(rawInput: unknown): SessionSource | undefin
|
|
|
29
31
|
switch (rawInput) {
|
|
30
32
|
case 'claude-code':
|
|
31
33
|
case 'opencode':
|
|
34
|
+
case 'codex':
|
|
35
|
+
case 'gemini':
|
|
32
36
|
return rawInput;
|
|
33
37
|
}
|
|
34
38
|
return;
|