@juspay/shooter 1.17.0 → 1.18.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.B0O0vCnX.css → 0.NV8k8wxG.css} +1 -1
- package/build/client/_app/immutable/assets/0.NV8k8wxG.css.br +0 -0
- package/build/client/_app/immutable/assets/{0.B0O0vCnX.css.gz → 0.NV8k8wxG.css.gz} +0 -0
- package/build/client/_app/immutable/chunks/{BctvtE4d.js → 8lO1IL7u.js} +1 -1
- package/build/client/_app/immutable/chunks/8lO1IL7u.js.br +0 -0
- package/build/client/_app/immutable/chunks/{BctvtE4d.js.gz → 8lO1IL7u.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js +1 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js.br +0 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js +1 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CjfxuHdN.js → DJvX78LW.js} +1 -1
- package/build/client/_app/immutable/chunks/DJvX78LW.js.br +0 -0
- package/build/client/_app/immutable/chunks/DJvX78LW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js +3 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js.br +0 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CNaTe-zm.js → app.f46Ko1hu.js} +2 -2
- package/build/client/_app/immutable/entry/app.f46Ko1hu.js.br +0 -0
- package/build/client/_app/immutable/entry/app.f46Ko1hu.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js +1 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.C3ELOf4c.js → 0.D_9EwVmq.js} +1 -1
- package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.Fqso94b3.js → 1.C4eFlqSB.js} +1 -1
- package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.BusCVJWk.js → 2.CdC092Za.js} +1 -1
- package/build/client/_app/immutable/nodes/2.CdC092Za.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CdC092Za.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.DUlpocIc.js → 3.Dhf4ZWW0.js} +1 -1
- package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.CG4eKRH0.js → 6.B3SEB_li.js} +1 -1
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.DHilxD1o.js → 7.DV8cJ1lX.js} +1 -1
- package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.BjKgvSie.js → 8.Bs362gyb.js} +2 -2
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.BRT6HOXB.js → 9.Cf7_3uqT.js} +1 -1
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.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-BWFSL107.js → 0-Cd7jY0a7.js} +3 -3
- package/build/server/chunks/{0-BWFSL107.js.map → 0-Cd7jY0a7.js.map} +1 -1
- package/build/server/chunks/{1-Bw5KlAjL.js → 1-C4BOGoJY.js} +2 -2
- package/build/server/chunks/{1-Bw5KlAjL.js.map → 1-C4BOGoJY.js.map} +1 -1
- package/build/server/chunks/{2-CQ3yYSVK.js → 2-Ba0mNwJ6.js} +2 -2
- package/build/server/chunks/{2-CQ3yYSVK.js.map → 2-Ba0mNwJ6.js.map} +1 -1
- package/build/server/chunks/{3-DZ4H9hPs.js → 3-Pg8t1uJU.js} +2 -2
- package/build/server/chunks/{3-DZ4H9hPs.js.map → 3-Pg8t1uJU.js.map} +1 -1
- package/build/server/chunks/{6-BZ0enR6b.js → 6-D8xbnTSo.js} +2 -2
- package/build/server/chunks/{6-BZ0enR6b.js.map → 6-D8xbnTSo.js.map} +1 -1
- package/build/server/chunks/{7-Lg8imTZn.js → 7-CkVK06S0.js} +2 -2
- package/build/server/chunks/{7-Lg8imTZn.js.map → 7-CkVK06S0.js.map} +1 -1
- package/build/server/chunks/{8-DKs4yOL7.js → 8-C8qVhrds.js} +2 -2
- package/build/server/chunks/{8-DKs4yOL7.js.map → 8-C8qVhrds.js.map} +1 -1
- package/build/server/chunks/{9-UNmpUWDY.js → 9-fL5zqN0T.js} +2 -2
- package/build/server/chunks/{9-UNmpUWDY.js.map → 9-fL5zqN0T.js.map} +1 -1
- package/build/server/chunks/{_server.ts-B1z0q6qZ.js → _server.ts-BA_uWcPw.js} +4 -5
- package/build/server/chunks/_server.ts-BA_uWcPw.js.map +1 -0
- package/build/server/chunks/{_server.ts-5wx4ZppI.js → _server.ts-Bu3s5hfv.js} +3 -3
- package/build/server/chunks/{_server.ts-5wx4ZppI.js.map → _server.ts-Bu3s5hfv.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CKXVBbwb.js → _server.ts-CwAjt91u.js} +8 -8
- package/build/server/chunks/_server.ts-CwAjt91u.js.map +1 -0
- package/build/server/chunks/{_server.ts-CgHc1Zpx.js → _server.ts-DZP2lhaY.js} +3 -3
- package/build/server/chunks/{_server.ts-CgHc1Zpx.js.map → _server.ts-DZP2lhaY.js.map} +1 -1
- package/build/server/chunks/{_server.ts-BMMTS86y.js → _server.ts-DZgfQKiH.js} +3 -4
- package/build/server/chunks/{_server.ts-BMMTS86y.js.map → _server.ts-DZgfQKiH.js.map} +1 -1
- package/build/server/chunks/{_server.ts-Bt7EAfjo.js → _server.ts-MbnroWEF.js} +25 -48
- package/build/server/chunks/_server.ts-MbnroWEF.js.map +1 -0
- package/build/server/chunks/{pty-manager-RmhVe2Ez.js → pty-manager-DmNSCKAr.js} +99 -2
- package/build/server/chunks/pty-manager-DmNSCKAr.js.map +1 -0
- package/build/server/chunks/qwen-reader-DGfUbKaJ.js +2112 -0
- package/build/server/chunks/qwen-reader-DGfUbKaJ.js.map +1 -0
- package/build/server/chunks/{registry-DzJj2E6I.js → registry-Kcw2UCMv.js} +55 -23
- package/build/server/chunks/registry-Kcw2UCMv.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +15 -15
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/scripts/e2e-all-features.sh +165 -0
- package/scripts/e2e-cross-terminal.sh +168 -0
- package/server.ts +12 -0
- package/src/lib/modules/client/common/provider.ts +0 -2
- package/src/lib/modules/client/terminal/ChatView.svelte +9 -2
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +3 -0
- package/src/lib/modules/server/sessions/amp-reader.ts +439 -0
- package/src/lib/modules/server/sessions/copilot-reader.ts +542 -0
- package/src/lib/modules/server/sessions/cursor-reader.ts +634 -0
- package/src/lib/modules/server/sessions/gemini-reader.ts +48 -25
- package/src/lib/modules/server/sessions/opencode-reader.ts +13 -12
- package/src/lib/modules/server/sessions/process-detector.ts +37 -60
- package/src/lib/modules/server/sessions/provider-paths.ts +173 -0
- package/src/lib/modules/server/sessions/qwen-reader.ts +41 -15
- package/src/lib/modules/server/sessions/registry.ts +55 -14
- package/src/lib/modules/server/terminal/generic-session-watcher.ts +163 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +51 -0
- package/src/lib/modules/server/ws/session-handler.ts +11 -1
- package/src/lib/theme.css +1 -2
- package/src/lib/types/generated/Sessions.ts +1 -4
- package/src/lib/types/server.ts +23 -6
- package/src/lib/types/sessions.ts +1 -10
- package/src/routes/api/sessions/connect/+server.ts +7 -3
- package/build/client/_app/immutable/assets/0.B0O0vCnX.css.br +0 -0
- package/build/client/_app/immutable/chunks/BctvtE4d.js.br +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js +0 -1
- package/build/client/_app/immutable/chunks/BxFShcQO.js.br +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ByzqAuXw.js +0 -3
- package/build/client/_app/immutable/chunks/ByzqAuXw.js.br +0 -0
- package/build/client/_app/immutable/chunks/ByzqAuXw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CjfxuHdN.js.br +0 -0
- package/build/client/_app/immutable/chunks/CjfxuHdN.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js +0 -1
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js.br +0 -0
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CNaTe-zm.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CNaTe-zm.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js +0 -1
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js.br +0 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Fqso94b3.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Fqso94b3.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DUlpocIc.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DUlpocIc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.gz +0 -0
- package/build/server/chunks/_server.ts-B1z0q6qZ.js.map +0 -1
- package/build/server/chunks/_server.ts-Bt7EAfjo.js.map +0 -1
- package/build/server/chunks/_server.ts-CKXVBbwb.js.map +0 -1
- package/build/server/chunks/opencode-db-path-BwaPufWf.js +0 -411
- package/build/server/chunks/opencode-db-path-BwaPufWf.js.map +0 -1
- package/build/server/chunks/pty-manager-RmhVe2Ez.js.map +0 -1
- package/build/server/chunks/qwen-reader-2fTFuC_D.js +0 -622
- package/build/server/chunks/qwen-reader-2fTFuC_D.js.map +0 -1
- package/build/server/chunks/registry-DzJj2E6I.js.map +0 -1
|
@@ -209,6 +209,32 @@ export function listGeminiProjects(): ProjectGroup[] {
|
|
|
209
209
|
);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Parse the full conversation from a single chats/session-*.json file.
|
|
214
|
+
* Returns all messages with no pagination — suitable for bulk processing.
|
|
215
|
+
*/
|
|
216
|
+
export function parseGeminiSessionFile(filePath: string): ConversationMessage[] {
|
|
217
|
+
try {
|
|
218
|
+
return conversationFromChatFile(filePath, 0, Number.MAX_SAFE_INTEGER);
|
|
219
|
+
} catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the absolute path of the chats/session-*.json file backing the
|
|
226
|
+
* given sessionId, or null if no chat file exists (e.g. logs.json-only session).
|
|
227
|
+
*/
|
|
228
|
+
export function resolveGeminiSessionFile(sessionId: string): null | string {
|
|
229
|
+
for (const hashDir of collectProjectHashDirs()) {
|
|
230
|
+
const chatFile = findChatFileForSession(hashDir, sessionId);
|
|
231
|
+
if (chatFile !== null) {
|
|
232
|
+
return chatFile;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
212
238
|
// ---------------------------------------------------------------------------
|
|
213
239
|
// Session building helpers
|
|
214
240
|
// ---------------------------------------------------------------------------
|
|
@@ -390,36 +416,33 @@ function extractTextFromContent(
|
|
|
390
416
|
}
|
|
391
417
|
|
|
392
418
|
/**
|
|
393
|
-
* Find the chats/session-*.json file for a specific sessionId.
|
|
394
|
-
*
|
|
395
|
-
*
|
|
419
|
+
* Find the chats/session-*.json file for a specific sessionId. The filename
|
|
420
|
+
* embeds the first 8 chars of the UUID, so prefix matches are tried first, but
|
|
421
|
+
* the exact sessionId field is always verified before returning — a short/empty
|
|
422
|
+
* id or a prefix collision must never mis-resolve to the wrong session.
|
|
396
423
|
*/
|
|
397
424
|
function findChatFileForSession(hashDir: string, sessionId: string): null | string {
|
|
398
|
-
|
|
425
|
+
if (!sessionId) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
399
428
|
const chatsDir = path.join(hashDir, 'chats');
|
|
429
|
+
let names: string[];
|
|
400
430
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
// Fast path: filename contains shortId.
|
|
407
|
-
if (entry.name.includes(shortId)) {
|
|
408
|
-
return path.join(chatsDir, entry.name);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
// Slow path: read each file and check the sessionId field.
|
|
412
|
-
for (const entry of entries) {
|
|
413
|
-
if (!entry.isFile() || !entry.name.startsWith('session-') || !entry.name.endsWith('.json')) {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
const record = readChatFile(path.join(chatsDir, entry.name));
|
|
417
|
-
if (record?.sessionId === sessionId) {
|
|
418
|
-
return path.join(chatsDir, entry.name);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
431
|
+
names = fs
|
|
432
|
+
.readdirSync(chatsDir, { withFileTypes: true })
|
|
433
|
+
.filter((e) => e.isFile() && e.name.startsWith('session-') && e.name.endsWith('.json'))
|
|
434
|
+
.map((e) => e.name);
|
|
421
435
|
} catch {
|
|
422
|
-
// chats dir missing or unreadable
|
|
436
|
+
return null; // chats dir missing or unreadable
|
|
437
|
+
}
|
|
438
|
+
const shortId = sessionId.slice(0, 8);
|
|
439
|
+
// Order prefix-matching filenames first (cheap), then verify the exact id.
|
|
440
|
+
names.sort((a, b) => Number(b.includes(shortId)) - Number(a.includes(shortId)));
|
|
441
|
+
for (const name of names) {
|
|
442
|
+
const full = path.join(chatsDir, name);
|
|
443
|
+
if (readChatFile(full)?.sessionId === sessionId) {
|
|
444
|
+
return full;
|
|
445
|
+
}
|
|
423
446
|
}
|
|
424
447
|
return null;
|
|
425
448
|
}
|
|
@@ -2,16 +2,6 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import * as crypto from 'crypto';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
|
|
5
|
-
function shortHash(input: string): string {
|
|
6
|
-
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
import type { ConversationMessage, MessagePart, ProjectGroup, SessionInfo } from '$lib/types';
|
|
10
|
-
|
|
11
|
-
import { resolveOpenCodeDbPath } from './opencode-db-path';
|
|
12
|
-
|
|
13
|
-
const OPENCODE_DB_PATH = resolveOpenCodeDbPath();
|
|
14
|
-
|
|
15
5
|
export function getOpenCodeConversation(
|
|
16
6
|
sessionId: string,
|
|
17
7
|
offset = 0,
|
|
@@ -127,6 +117,10 @@ export function getOpenCodeConversation(
|
|
|
127
117
|
}
|
|
128
118
|
}
|
|
129
119
|
|
|
120
|
+
import type { ConversationMessage, MessagePart, ProjectGroup, SessionInfo } from '$lib/types';
|
|
121
|
+
|
|
122
|
+
import { resolveOpenCodeDbPath } from './opencode-db-path';
|
|
123
|
+
|
|
130
124
|
export function listOpenCodeProjects(): ProjectGroup[] {
|
|
131
125
|
const db = getDb();
|
|
132
126
|
if (!db) {
|
|
@@ -251,12 +245,19 @@ function convertOpenCodePart(data: Record<string, unknown>): MessagePart | null
|
|
|
251
245
|
}
|
|
252
246
|
|
|
253
247
|
function getDb(): Database.Database | null {
|
|
254
|
-
|
|
248
|
+
// Resolve per call (not at module load) so a DB created after server start —
|
|
249
|
+
// or an XDG_DATA_HOME change — is still picked up.
|
|
250
|
+
const dbPath = resolveOpenCodeDbPath();
|
|
251
|
+
if (!fs.existsSync(dbPath)) {
|
|
255
252
|
return null;
|
|
256
253
|
}
|
|
257
254
|
try {
|
|
258
|
-
return new Database(
|
|
255
|
+
return new Database(dbPath, { readonly: true });
|
|
259
256
|
} catch {
|
|
260
257
|
return null;
|
|
261
258
|
}
|
|
262
259
|
}
|
|
260
|
+
|
|
261
|
+
function shortHash(input: string): string {
|
|
262
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
263
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ClaudeSessionFile, DetectedProcess } from '$lib/types';
|
|
2
2
|
|
|
3
|
+
import { detectActiveAmpSessions } from '$lib/modules/server/sessions/amp-reader';
|
|
3
4
|
import { detectActiveCodexSessions } from '$lib/modules/server/sessions/codex-reader';
|
|
5
|
+
import { detectActiveCopilotSessions } from '$lib/modules/server/sessions/copilot-reader';
|
|
6
|
+
import { detectActiveCursorSessions } from '$lib/modules/server/sessions/cursor-reader';
|
|
4
7
|
import { detectActiveGeminiSessions } from '$lib/modules/server/sessions/gemini-reader';
|
|
5
8
|
import { resolveOpenCodeDbPath } from '$lib/modules/server/sessions/opencode-db-path';
|
|
6
9
|
import { detectActiveQwenSessions } from '$lib/modules/server/sessions/qwen-reader';
|
|
@@ -48,14 +51,22 @@ const CLAUDE_SESSIONS_DIR = join(homedir(), '.claude', 'sessions');
|
|
|
48
51
|
// OpenCode sessions updated within this window are considered "live"
|
|
49
52
|
const OPENCODE_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
50
53
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// File-based providers: a session file written within this window = "live".
|
|
55
|
+
const FILE_PROVIDER_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
56
|
+
|
|
57
|
+
// File-based providers (no PID file) share one detection shape:
|
|
58
|
+
// detectActive<P>Sessions(thresholdMs) -> { cwd, id, startedAt }[].
|
|
59
|
+
const FILE_PROVIDER_DETECTORS: {
|
|
60
|
+
command: DetectedProcess['command'];
|
|
61
|
+
detect: (thresholdMs: number) => { cwd: string; id: string; startedAt: number }[];
|
|
62
|
+
}[] = [
|
|
63
|
+
{ command: 'codex', detect: detectActiveCodexSessions },
|
|
64
|
+
{ command: 'gemini', detect: detectActiveGeminiSessions },
|
|
65
|
+
{ command: 'qwen', detect: detectActiveQwenSessions },
|
|
66
|
+
{ command: 'cursor-agent', detect: detectActiveCursorSessions },
|
|
67
|
+
{ command: 'copilot', detect: detectActiveCopilotSessions },
|
|
68
|
+
{ command: 'amp', detect: detectActiveAmpSessions },
|
|
69
|
+
];
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
72
|
* Scan ~/.claude/sessions/*.json to find running Claude Code processes,
|
|
@@ -149,59 +160,25 @@ export function detectRunningAISessions(): DetectedProcess[] {
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
// --- Codex
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// --- Gemini sessions ---
|
|
172
|
-
// Gemini has no PID file; a logs.json / chat file written in the last few
|
|
173
|
-
// minutes indicates an active session. cwd is reverse-mapped where possible.
|
|
174
|
-
try {
|
|
175
|
-
for (const s of detectActiveGeminiSessions(GEMINI_ACTIVE_THRESHOLD_MS)) {
|
|
176
|
-
results.push({
|
|
177
|
-
command: 'gemini',
|
|
178
|
-
cwd: s.cwd,
|
|
179
|
-
kind: 'interactive',
|
|
180
|
-
pid: 0, // Gemini doesn't expose a per-session PID
|
|
181
|
-
projectPath: cwdToProjectPath(s.cwd),
|
|
182
|
-
sessionId: s.id,
|
|
183
|
-
startedAt: s.startedAt,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
// ~/.gemini/tmp missing or unreadable — skip silently
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// --- Qwen sessions ---
|
|
191
|
-
try {
|
|
192
|
-
for (const s of detectActiveQwenSessions(QWEN_ACTIVE_THRESHOLD_MS)) {
|
|
193
|
-
results.push({
|
|
194
|
-
command: 'qwen',
|
|
195
|
-
cwd: s.cwd,
|
|
196
|
-
kind: 'interactive',
|
|
197
|
-
pid: 0,
|
|
198
|
-
projectPath: cwdToProjectPath(s.cwd),
|
|
199
|
-
sessionId: s.id,
|
|
200
|
-
startedAt: s.startedAt,
|
|
201
|
-
});
|
|
163
|
+
// --- File-based providers (Codex/Gemini/Qwen/Cursor/Copilot/Amp) ---
|
|
164
|
+
// None expose a PID; a recently-written session file means "live". cwd/id come
|
|
165
|
+
// from each provider's reader. One loop instead of a block per provider.
|
|
166
|
+
for (const { command, detect } of FILE_PROVIDER_DETECTORS) {
|
|
167
|
+
try {
|
|
168
|
+
for (const s of detect(FILE_PROVIDER_ACTIVE_THRESHOLD_MS)) {
|
|
169
|
+
results.push({
|
|
170
|
+
command,
|
|
171
|
+
cwd: s.cwd,
|
|
172
|
+
kind: 'interactive',
|
|
173
|
+
pid: 0,
|
|
174
|
+
projectPath: cwdToProjectPath(s.cwd),
|
|
175
|
+
sessionId: s.id,
|
|
176
|
+
startedAt: s.startedAt,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// provider session dir missing/unreadable — skip silently
|
|
202
181
|
}
|
|
203
|
-
} catch {
|
|
204
|
-
// ~/.qwen/projects missing or unreadable — skip silently
|
|
205
182
|
}
|
|
206
183
|
|
|
207
184
|
// Sort by startedAt descending (most recent first)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-based dispatch for the five read-only providers (cursor, copilot,
|
|
3
|
+
* qwen, gemini, amp). These have no incremental byte watcher of their own;
|
|
4
|
+
* the generic-session-watcher re-reads the whole file on change and parses it
|
|
5
|
+
* through here. Keeping all path↔provider knowledge in one module means the
|
|
6
|
+
* watcher, the WS session handler, and the SoS coordinator share one source of
|
|
7
|
+
* truth and never branch on provider directories themselves.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ConversationMessage, SessionSource } from '$lib/types';
|
|
11
|
+
|
|
12
|
+
import { detectActiveAmpSessions, parseAmpSessionFile, resolveAmpSessionFile } from './amp-reader';
|
|
13
|
+
import {
|
|
14
|
+
detectActiveCopilotSessions,
|
|
15
|
+
parseCopilotSessionFile,
|
|
16
|
+
resolveCopilotSessionFile,
|
|
17
|
+
} from './copilot-reader';
|
|
18
|
+
import {
|
|
19
|
+
detectActiveCursorSessions,
|
|
20
|
+
parseCursorSessionFile,
|
|
21
|
+
resolveCursorSessionFile,
|
|
22
|
+
} from './cursor-reader';
|
|
23
|
+
import {
|
|
24
|
+
detectActiveGeminiSessions,
|
|
25
|
+
parseGeminiSessionFile,
|
|
26
|
+
resolveGeminiSessionFile,
|
|
27
|
+
} from './gemini-reader';
|
|
28
|
+
import {
|
|
29
|
+
detectActiveQwenSessions,
|
|
30
|
+
parseQwenSessionFile,
|
|
31
|
+
resolveQwenSessionFile,
|
|
32
|
+
} from './qwen-reader';
|
|
33
|
+
|
|
34
|
+
/** True when the path belongs to one of the five read-only providers. */
|
|
35
|
+
export function isReadOnlyProviderPath(filePath: string): boolean {
|
|
36
|
+
return readOnlyProviderForPath(filePath) !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a single read-only-provider session file (by path) into the full
|
|
41
|
+
* conversation. Returns [] for unknown paths or on any read/parse error.
|
|
42
|
+
*/
|
|
43
|
+
export function parseReadOnlyProviderFile(filePath: string): ConversationMessage[] {
|
|
44
|
+
switch (readOnlyProviderForPath(filePath)) {
|
|
45
|
+
case 'amp':
|
|
46
|
+
return parseAmpSessionFile(filePath);
|
|
47
|
+
case 'copilot':
|
|
48
|
+
return parseCopilotSessionFile(filePath);
|
|
49
|
+
case 'cursor':
|
|
50
|
+
return parseCursorSessionFile(filePath);
|
|
51
|
+
case 'gemini':
|
|
52
|
+
return parseGeminiSessionFile(filePath);
|
|
53
|
+
case 'qwen':
|
|
54
|
+
return parseQwenSessionFile(filePath);
|
|
55
|
+
default:
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Identify which read-only provider backs a given session-file path, or null
|
|
62
|
+
* for claude/codex/opencode (which have their own watchers) and unknown paths.
|
|
63
|
+
* Detection is by the provider's root directory, which is unique per provider.
|
|
64
|
+
*/
|
|
65
|
+
export function readOnlyProviderForPath(filePath: string): null | SessionSource {
|
|
66
|
+
if (filePath.includes('/.cursor/')) {
|
|
67
|
+
return 'cursor';
|
|
68
|
+
}
|
|
69
|
+
if (filePath.includes('/.copilot/')) {
|
|
70
|
+
return 'copilot';
|
|
71
|
+
}
|
|
72
|
+
if (filePath.includes('/.qwen/')) {
|
|
73
|
+
return 'qwen';
|
|
74
|
+
}
|
|
75
|
+
if (filePath.includes('/.gemini/')) {
|
|
76
|
+
return 'gemini';
|
|
77
|
+
}
|
|
78
|
+
if (filePath.includes('/amp/threads/')) {
|
|
79
|
+
return 'amp';
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Map a launchable CLI command to its read-only provider source, or null. */
|
|
85
|
+
export function readOnlySourceForCommand(command: string): null | SessionSource {
|
|
86
|
+
switch (command) {
|
|
87
|
+
case 'amp':
|
|
88
|
+
return 'amp';
|
|
89
|
+
case 'copilot':
|
|
90
|
+
return 'copilot';
|
|
91
|
+
case 'cursor-agent':
|
|
92
|
+
return 'cursor';
|
|
93
|
+
case 'gemini':
|
|
94
|
+
return 'gemini';
|
|
95
|
+
case 'qwen':
|
|
96
|
+
return 'qwen';
|
|
97
|
+
default:
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a read-only provider's session ID to its backing file path, or null
|
|
104
|
+
* if the provider has no single-file backing for that session (e.g. a Gemini
|
|
105
|
+
* session present only in logs.json) or the ID is not found.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveReadOnlyProviderFile(
|
|
108
|
+
source: SessionSource,
|
|
109
|
+
sessionId: string
|
|
110
|
+
): null | string {
|
|
111
|
+
switch (source) {
|
|
112
|
+
case 'amp':
|
|
113
|
+
return resolveAmpSessionFile(sessionId);
|
|
114
|
+
case 'copilot':
|
|
115
|
+
return resolveCopilotSessionFile(sessionId);
|
|
116
|
+
case 'cursor':
|
|
117
|
+
return resolveCursorSessionFile(sessionId);
|
|
118
|
+
case 'gemini':
|
|
119
|
+
return resolveGeminiSessionFile(sessionId);
|
|
120
|
+
case 'qwen':
|
|
121
|
+
return resolveQwenSessionFile(sessionId);
|
|
122
|
+
default:
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const READ_ONLY_DETECTORS: Partial<
|
|
128
|
+
Record<SessionSource, (thresholdMs: number) => { cwd: string; id: string; startedAt: number }[]>
|
|
129
|
+
> = {
|
|
130
|
+
amp: detectActiveAmpSessions,
|
|
131
|
+
copilot: detectActiveCopilotSessions,
|
|
132
|
+
cursor: detectActiveCursorSessions,
|
|
133
|
+
gemini: detectActiveGeminiSessions,
|
|
134
|
+
qwen: detectActiveQwenSessions,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* After Shooter launches a read-only-provider CLI, find the session file it
|
|
139
|
+
* just created so the terminal can be live-tailed. Picks the most recent
|
|
140
|
+
* session started at/after launch, preferring an exact cwd match (cwd decoding
|
|
141
|
+
* is heuristic for some providers, so a same-provider, started-after-launch
|
|
142
|
+
* session is the fallback). Returns null until the CLI has written a session.
|
|
143
|
+
*/
|
|
144
|
+
export function discoverReadOnlyProviderSessionFile(
|
|
145
|
+
source: SessionSource,
|
|
146
|
+
cwd: string,
|
|
147
|
+
launchTimeMs: number,
|
|
148
|
+
nowMs: number
|
|
149
|
+
): null | string {
|
|
150
|
+
const detect = READ_ONLY_DETECTORS[source];
|
|
151
|
+
if (!detect) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
// Detector scan window: reach back to ~60s before launch so the just-created
|
|
155
|
+
// session is still inside the mtime window however long ago we launched.
|
|
156
|
+
const thresholdMs = Math.max(nowMs - launchTimeMs + 60_000, 60_000);
|
|
157
|
+
let active: { cwd: string; id: string; startedAt: number }[];
|
|
158
|
+
try {
|
|
159
|
+
active = detect(thresholdMs);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// Start-time tolerance (distinct from the scan window above): keep only
|
|
164
|
+
// sessions that started at/after launch, with 2s of clock-skew slack.
|
|
165
|
+
const afterLaunch = active
|
|
166
|
+
.filter((s) => s.startedAt >= launchTimeMs - 2000)
|
|
167
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
168
|
+
const match = afterLaunch.find((s) => s.cwd === cwd) ?? afterLaunch[0];
|
|
169
|
+
if (!match) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return resolveReadOnlyProviderFile(source, match.id);
|
|
173
|
+
}
|
|
@@ -68,21 +68,7 @@ export function getQwenConversation(
|
|
|
68
68
|
return [];
|
|
69
69
|
}
|
|
70
70
|
try {
|
|
71
|
-
const messages
|
|
72
|
-
for (const line of readQwenTextBounded(filePath).split('\n')) {
|
|
73
|
-
const trimmed = line.trim();
|
|
74
|
-
if (!trimmed) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const msg = qwenLineToMessage(JSON.parse(trimmed) as Record<string, unknown>);
|
|
79
|
-
if (msg) {
|
|
80
|
-
messages.push(msg);
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// skip malformed line
|
|
84
|
-
}
|
|
85
|
-
}
|
|
71
|
+
const messages = readQwenMessages(filePath);
|
|
86
72
|
if (offset === 0 && messages.length > limit) {
|
|
87
73
|
let startIdx = messages.length - limit;
|
|
88
74
|
while (startIdx > 0 && messages[startIdx].role !== 'user') {
|
|
@@ -148,6 +134,23 @@ export function listQwenProjects(): ProjectGroup[] {
|
|
|
148
134
|
);
|
|
149
135
|
}
|
|
150
136
|
|
|
137
|
+
/** Parse a Qwen session file into messages (used by the generic read-only watcher); [] on error. */
|
|
138
|
+
export function parseQwenSessionFile(filePath: string): ConversationMessage[] {
|
|
139
|
+
try {
|
|
140
|
+
return readQwenMessages(filePath);
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Resolve a Qwen session id to its `chats/*.jsonl` path, or null if not found. */
|
|
147
|
+
export function resolveQwenSessionFile(sessionId: string): null | string {
|
|
148
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return collectQwenFiles().find((p) => path.basename(p) === `${sessionId}.jsonl`) ?? null;
|
|
152
|
+
}
|
|
153
|
+
|
|
151
154
|
/** All chats/*.jsonl files under ~/.qwen/projects/<encoded-cwd>/chats/. */
|
|
152
155
|
function collectQwenFiles(): string[] {
|
|
153
156
|
const out: string[] = [];
|
|
@@ -286,6 +289,29 @@ function readPrefix(filePath: string): string {
|
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Read all ConversationMessages from a Qwen JSONL file without pagination.
|
|
294
|
+
* Oversized files are tail-bounded (see readQwenTextBounded) to cap memory.
|
|
295
|
+
*/
|
|
296
|
+
function readQwenMessages(filePath: string): ConversationMessage[] {
|
|
297
|
+
const messages: ConversationMessage[] = [];
|
|
298
|
+
for (const line of readQwenTextBounded(filePath).split('\n')) {
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (!trimmed) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const msg = qwenLineToMessage(JSON.parse(trimmed) as Record<string, unknown>);
|
|
305
|
+
if (msg) {
|
|
306
|
+
messages.push(msg);
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// skip malformed line
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return messages;
|
|
313
|
+
}
|
|
314
|
+
|
|
289
315
|
/** Read a Qwen session file, bounded to the tail for oversized files to cap memory. */
|
|
290
316
|
function readQwenTextBounded(filePath: string): string {
|
|
291
317
|
const size = fs.statSync(filePath).size;
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ConversationMessage, ProjectGroup, ProviderDef } from '$lib/types';
|
|
14
14
|
|
|
15
|
+
import { getAmpConversation, listAmpProjects } from './amp-reader';
|
|
15
16
|
import { getCodexConversation, listCodexProjects } from './codex-reader';
|
|
17
|
+
import { getCopilotConversation, listCopilotProjects } from './copilot-reader';
|
|
18
|
+
import { getCursorConversation, listCursorProjects } from './cursor-reader';
|
|
16
19
|
import { getGeminiConversation, listGeminiProjects } from './gemini-reader';
|
|
17
20
|
import { getSessionConversation, listProjectsWithSessions } from './jsonl-reader';
|
|
18
21
|
import { getOpenCodeConversation, listOpenCodeProjects } from './opencode-reader';
|
|
@@ -70,6 +73,33 @@ export const PROVIDERS: ProviderDef[] = [
|
|
|
70
73
|
resumeArgs: () => [],
|
|
71
74
|
source: 'qwen',
|
|
72
75
|
},
|
|
76
|
+
{
|
|
77
|
+
command: 'cursor-agent',
|
|
78
|
+
getConversation: getCursorConversation,
|
|
79
|
+
isAI: true,
|
|
80
|
+
label: 'Cursor',
|
|
81
|
+
listProjects: listCursorProjects,
|
|
82
|
+
resumeArgs: () => [],
|
|
83
|
+
source: 'cursor',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
command: 'copilot',
|
|
87
|
+
getConversation: getCopilotConversation,
|
|
88
|
+
isAI: true,
|
|
89
|
+
label: 'Copilot',
|
|
90
|
+
listProjects: listCopilotProjects,
|
|
91
|
+
resumeArgs: () => [],
|
|
92
|
+
source: 'copilot',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
command: 'amp',
|
|
96
|
+
getConversation: getAmpConversation,
|
|
97
|
+
isAI: true,
|
|
98
|
+
label: 'Amp',
|
|
99
|
+
listProjects: listAmpProjects,
|
|
100
|
+
resumeArgs: () => [],
|
|
101
|
+
source: 'amp',
|
|
102
|
+
},
|
|
73
103
|
];
|
|
74
104
|
|
|
75
105
|
/** AI-agent binary names (for AI_COMMANDS-style checks). */
|
|
@@ -93,9 +123,13 @@ export function getProviderConversation(
|
|
|
93
123
|
if (provider.source === 'claude-code') {
|
|
94
124
|
continue;
|
|
95
125
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
try {
|
|
127
|
+
const messages = provider.getConversation(sessionId, offset, limit);
|
|
128
|
+
if (messages.length > 0) {
|
|
129
|
+
return messages;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// a failing provider reader must not break resolution — try the next
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
return [];
|
|
@@ -112,17 +146,24 @@ export function listAllProviderProjects(): ProjectGroup[] {
|
|
|
112
146
|
continue; // a broken provider must not take down the whole listing
|
|
113
147
|
}
|
|
114
148
|
for (const group of groups) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
try {
|
|
150
|
+
const name =
|
|
151
|
+
provider.nameSuffix && typeof group.name === 'string'
|
|
152
|
+
? group.name.replace(provider.nameSuffix, '')
|
|
153
|
+
: group.name;
|
|
154
|
+
const existing = byPath.get(group.fullPath);
|
|
155
|
+
if (existing) {
|
|
156
|
+
existing.sessions.push(...group.sessions);
|
|
157
|
+
existing.sessions.sort(
|
|
158
|
+
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
|
159
|
+
);
|
|
160
|
+
existing.sessionCount = existing.sessions.length;
|
|
161
|
+
existing.lastModified = existing.sessions[0]?.modified || existing.lastModified;
|
|
162
|
+
} else {
|
|
163
|
+
byPath.set(group.fullPath, { ...group, name });
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// skip a malformed group rather than dropping the rest of the provider
|
|
126
167
|
}
|
|
127
168
|
}
|
|
128
169
|
}
|