@juspay/shooter 1.16.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.
Files changed (175) hide show
  1. package/.claude/hooks/codex-hooks.example.json +75 -0
  2. package/.claude/hooks/notifier.cjs +158 -8
  3. package/build/client/_app/immutable/assets/{0.DEfoFaGR.css → 0.NV8k8wxG.css} +1 -1
  4. package/build/client/_app/immutable/assets/0.NV8k8wxG.css.br +0 -0
  5. package/build/client/_app/immutable/assets/0.NV8k8wxG.css.gz +0 -0
  6. package/build/client/_app/immutable/chunks/8lO1IL7u.js +1 -0
  7. package/build/client/_app/immutable/chunks/8lO1IL7u.js.br +0 -0
  8. package/build/client/_app/immutable/chunks/8lO1IL7u.js.gz +0 -0
  9. package/build/client/_app/immutable/chunks/B9WQy_3X.js +1 -0
  10. package/build/client/_app/immutable/chunks/B9WQy_3X.js.br +0 -0
  11. package/build/client/_app/immutable/chunks/B9WQy_3X.js.gz +0 -0
  12. package/build/client/_app/immutable/chunks/BdtLzPpO.js +1 -0
  13. package/build/client/_app/immutable/chunks/BdtLzPpO.js.br +0 -0
  14. package/build/client/_app/immutable/chunks/BdtLzPpO.js.gz +0 -0
  15. package/build/client/_app/immutable/chunks/{DlS3abGJ.js → DJvX78LW.js} +1 -1
  16. package/build/client/_app/immutable/chunks/DJvX78LW.js.br +0 -0
  17. package/build/client/_app/immutable/chunks/DJvX78LW.js.gz +0 -0
  18. package/build/client/_app/immutable/chunks/nWG9RHyB.js +3 -0
  19. package/build/client/_app/immutable/chunks/nWG9RHyB.js.br +0 -0
  20. package/build/client/_app/immutable/chunks/nWG9RHyB.js.gz +0 -0
  21. package/build/client/_app/immutable/entry/{app.CSJG7N9H.js → app.f46Ko1hu.js} +2 -2
  22. package/build/client/_app/immutable/entry/app.f46Ko1hu.js.br +0 -0
  23. package/build/client/_app/immutable/entry/app.f46Ko1hu.js.gz +0 -0
  24. package/build/client/_app/immutable/entry/start.BVDjNnXt.js +1 -0
  25. package/build/client/_app/immutable/entry/start.BVDjNnXt.js.br +2 -0
  26. package/build/client/_app/immutable/entry/start.BVDjNnXt.js.gz +0 -0
  27. package/build/client/_app/immutable/nodes/{0.qOL7xtFn.js → 0.D_9EwVmq.js} +1 -1
  28. package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.br +0 -0
  29. package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.gz +0 -0
  30. package/build/client/_app/immutable/nodes/{1.Di708Ago.js → 1.C4eFlqSB.js} +1 -1
  31. package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.br +0 -0
  32. package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.gz +0 -0
  33. package/build/client/_app/immutable/nodes/{2.DSM1znqa.js → 2.CdC092Za.js} +1 -1
  34. package/build/client/_app/immutable/nodes/2.CdC092Za.js.br +0 -0
  35. package/build/client/_app/immutable/nodes/2.CdC092Za.js.gz +0 -0
  36. package/build/client/_app/immutable/nodes/{3.BPa5fh75.js → 3.Dhf4ZWW0.js} +1 -1
  37. package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.br +0 -0
  38. package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.gz +0 -0
  39. package/build/client/_app/immutable/nodes/6.B3SEB_li.js +1 -0
  40. package/build/client/_app/immutable/nodes/6.B3SEB_li.js.br +0 -0
  41. package/build/client/_app/immutable/nodes/6.B3SEB_li.js.gz +0 -0
  42. package/build/client/_app/immutable/nodes/{7.B7UJd8GQ.js → 7.DV8cJ1lX.js} +3 -3
  43. package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.br +0 -0
  44. package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.gz +0 -0
  45. package/build/client/_app/immutable/nodes/8.Bs362gyb.js +2 -0
  46. package/build/client/_app/immutable/nodes/8.Bs362gyb.js.br +0 -0
  47. package/build/client/_app/immutable/nodes/8.Bs362gyb.js.gz +0 -0
  48. package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js +2 -0
  49. package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js.br +0 -0
  50. package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js.gz +0 -0
  51. package/build/client/_app/version.json +1 -1
  52. package/build/client/_app/version.json.br +0 -0
  53. package/build/client/_app/version.json.gz +0 -0
  54. package/build/server/chunks/{0-D8uPamNd.js → 0-Cd7jY0a7.js} +3 -3
  55. package/build/server/chunks/{0-D8uPamNd.js.map → 0-Cd7jY0a7.js.map} +1 -1
  56. package/build/server/chunks/{1-DhtioHbs.js → 1-C4BOGoJY.js} +2 -2
  57. package/build/server/chunks/{1-DhtioHbs.js.map → 1-C4BOGoJY.js.map} +1 -1
  58. package/build/server/chunks/{2-Cgh7ZFgE.js → 2-Ba0mNwJ6.js} +2 -2
  59. package/build/server/chunks/{2-Cgh7ZFgE.js.map → 2-Ba0mNwJ6.js.map} +1 -1
  60. package/build/server/chunks/{3-I6hnjssH.js → 3-Pg8t1uJU.js} +2 -2
  61. package/build/server/chunks/{3-I6hnjssH.js.map → 3-Pg8t1uJU.js.map} +1 -1
  62. package/build/server/chunks/{6-bPDbH1_W.js → 6-D8xbnTSo.js} +2 -2
  63. package/build/server/chunks/6-D8xbnTSo.js.map +1 -0
  64. package/build/server/chunks/{7-CpBrOkxQ.js → 7-CkVK06S0.js} +2 -2
  65. package/build/server/chunks/7-CkVK06S0.js.map +1 -0
  66. package/build/server/chunks/{8-BRGAVfze.js → 8-C8qVhrds.js} +2 -2
  67. package/build/server/chunks/8-C8qVhrds.js.map +1 -0
  68. package/build/server/chunks/{9-C6xuAb_Y.js → 9-fL5zqN0T.js} +2 -2
  69. package/build/server/chunks/9-fL5zqN0T.js.map +1 -0
  70. package/build/server/chunks/{_server.ts-BrRZXr-8.js → _server.ts-BA_uWcPw.js} +9 -9
  71. package/build/server/chunks/_server.ts-BA_uWcPw.js.map +1 -0
  72. package/build/server/chunks/{_server.ts-C6xbNz6d.js → _server.ts-Bu3s5hfv.js} +3 -3
  73. package/build/server/chunks/{_server.ts-C6xbNz6d.js.map → _server.ts-Bu3s5hfv.js.map} +1 -1
  74. package/build/server/chunks/{_server.ts-Cq9_scaV.js → _server.ts-CwAjt91u.js} +18 -18
  75. package/build/server/chunks/_server.ts-CwAjt91u.js.map +1 -0
  76. package/build/server/chunks/{_server.ts-CFX-S_8q.js → _server.ts-DZ5naqSL.js} +2 -2
  77. package/build/server/chunks/{_server.ts-CFX-S_8q.js.map → _server.ts-DZ5naqSL.js.map} +1 -1
  78. package/build/server/chunks/{_server.ts-Dekgb6Hx.js → _server.ts-DZP2lhaY.js} +3 -3
  79. package/build/server/chunks/{_server.ts-Dekgb6Hx.js.map → _server.ts-DZP2lhaY.js.map} +1 -1
  80. package/build/server/chunks/_server.ts-DZgfQKiH.js +81 -0
  81. package/build/server/chunks/_server.ts-DZgfQKiH.js.map +1 -0
  82. package/build/server/chunks/{_server.ts-CjK0g9dO.js → _server.ts-MbnroWEF.js} +25 -16
  83. package/build/server/chunks/_server.ts-MbnroWEF.js.map +1 -0
  84. package/build/server/chunks/{pty-manager-aFpChJah.js → pty-manager-DmNSCKAr.js} +99 -2
  85. package/build/server/chunks/pty-manager-DmNSCKAr.js.map +1 -0
  86. package/build/server/chunks/qwen-reader-DGfUbKaJ.js +2112 -0
  87. package/build/server/chunks/qwen-reader-DGfUbKaJ.js.map +1 -0
  88. package/build/server/chunks/{_server.ts-D--_NXt2.js → registry-Kcw2UCMv.js} +132 -106
  89. package/build/server/chunks/registry-Kcw2UCMv.js.map +1 -0
  90. package/build/server/index.js +1 -1
  91. package/build/server/index.js.map +1 -1
  92. package/build/server/manifest.js +16 -16
  93. package/build/server/manifest.js.map +1 -1
  94. package/package.json +2 -2
  95. package/scripts/e2e-all-features.sh +165 -0
  96. package/scripts/e2e-cross-terminal.sh +168 -0
  97. package/server.ts +12 -0
  98. package/src/lib/modules/client/common/index.ts +1 -1
  99. package/src/lib/modules/client/common/provider.ts +11 -0
  100. package/src/lib/modules/client/terminal/ChatView.svelte +9 -2
  101. package/src/lib/modules/client/terminal/LaunchSheet.svelte +4 -0
  102. package/src/lib/modules/server/sessions/amp-reader.ts +439 -0
  103. package/src/lib/modules/server/sessions/codex-reader.ts +34 -33
  104. package/src/lib/modules/server/sessions/copilot-reader.ts +542 -0
  105. package/src/lib/modules/server/sessions/cursor-reader.ts +634 -0
  106. package/src/lib/modules/server/sessions/gemini-reader.ts +594 -0
  107. package/src/lib/modules/server/sessions/opencode-db-path.ts +19 -10
  108. package/src/lib/modules/server/sessions/opencode-reader.ts +13 -12
  109. package/src/lib/modules/server/sessions/process-detector.ts +39 -18
  110. package/src/lib/modules/server/sessions/provider-paths.ts +173 -0
  111. package/src/lib/modules/server/sessions/qwen-reader.ts +336 -0
  112. package/src/lib/modules/server/sessions/registry.ts +178 -0
  113. package/src/lib/modules/server/terminal/codex-watcher.ts +4 -1
  114. package/src/lib/modules/server/terminal/generic-session-watcher.ts +163 -0
  115. package/src/lib/modules/server/terminal/pty-manager.ts +51 -0
  116. package/src/lib/modules/server/ws/session-handler.ts +34 -20
  117. package/src/lib/theme.css +32 -0
  118. package/src/lib/types/gemini.ts +100 -0
  119. package/src/lib/types/generated/Sessions.ts +17 -1
  120. package/src/lib/types/index.ts +1 -0
  121. package/src/lib/types/server.ts +23 -6
  122. package/src/lib/types/sessions.ts +14 -2
  123. package/src/routes/api/sessions/+server.ts +5 -52
  124. package/src/routes/api/sessions/connect/+server.ts +18 -11
  125. package/src/routes/api/terminals/+server.ts +7 -5
  126. package/src/routes/terminals/+page.svelte +7 -2
  127. package/src/routes/terminals/[id]/+page.svelte +1 -2
  128. package/build/client/_app/immutable/assets/0.DEfoFaGR.css.br +0 -0
  129. package/build/client/_app/immutable/assets/0.DEfoFaGR.css.gz +0 -0
  130. package/build/client/_app/immutable/chunks/Bkqjn62J.js +0 -1
  131. package/build/client/_app/immutable/chunks/Bkqjn62J.js.br +0 -1
  132. package/build/client/_app/immutable/chunks/Bkqjn62J.js.gz +0 -0
  133. package/build/client/_app/immutable/chunks/DOHhmtDH.js +0 -3
  134. package/build/client/_app/immutable/chunks/DOHhmtDH.js.br +0 -0
  135. package/build/client/_app/immutable/chunks/DOHhmtDH.js.gz +0 -0
  136. package/build/client/_app/immutable/chunks/DlS3abGJ.js.br +0 -0
  137. package/build/client/_app/immutable/chunks/DlS3abGJ.js.gz +0 -0
  138. package/build/client/_app/immutable/chunks/Pw0jDB7M.js +0 -1
  139. package/build/client/_app/immutable/chunks/Pw0jDB7M.js.br +0 -0
  140. package/build/client/_app/immutable/chunks/Pw0jDB7M.js.gz +0 -0
  141. package/build/client/_app/immutable/entry/app.CSJG7N9H.js.br +0 -0
  142. package/build/client/_app/immutable/entry/app.CSJG7N9H.js.gz +0 -0
  143. package/build/client/_app/immutable/entry/start.CTt1901T.js +0 -1
  144. package/build/client/_app/immutable/entry/start.CTt1901T.js.br +0 -2
  145. package/build/client/_app/immutable/entry/start.CTt1901T.js.gz +0 -0
  146. package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.br +0 -0
  147. package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.gz +0 -0
  148. package/build/client/_app/immutable/nodes/1.Di708Ago.js.br +0 -0
  149. package/build/client/_app/immutable/nodes/1.Di708Ago.js.gz +0 -0
  150. package/build/client/_app/immutable/nodes/2.DSM1znqa.js.br +0 -0
  151. package/build/client/_app/immutable/nodes/2.DSM1znqa.js.gz +0 -0
  152. package/build/client/_app/immutable/nodes/3.BPa5fh75.js.br +0 -0
  153. package/build/client/_app/immutable/nodes/3.BPa5fh75.js.gz +0 -0
  154. package/build/client/_app/immutable/nodes/6.B1LwwEF-.js +0 -1
  155. package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.br +0 -0
  156. package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.gz +0 -0
  157. package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.br +0 -0
  158. package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.gz +0 -0
  159. package/build/client/_app/immutable/nodes/8.CG0mrgBU.js +0 -2
  160. package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.br +0 -0
  161. package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.gz +0 -0
  162. package/build/client/_app/immutable/nodes/9.KwzWaMHj.js +0 -2
  163. package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.br +0 -0
  164. package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.gz +0 -0
  165. package/build/server/chunks/6-bPDbH1_W.js.map +0 -1
  166. package/build/server/chunks/7-CpBrOkxQ.js.map +0 -1
  167. package/build/server/chunks/8-BRGAVfze.js.map +0 -1
  168. package/build/server/chunks/9-C6xuAb_Y.js.map +0 -1
  169. package/build/server/chunks/_server.ts-BrRZXr-8.js.map +0 -1
  170. package/build/server/chunks/_server.ts-CjK0g9dO.js.map +0 -1
  171. package/build/server/chunks/_server.ts-Cq9_scaV.js.map +0 -1
  172. package/build/server/chunks/_server.ts-D--_NXt2.js.map +0 -1
  173. package/build/server/chunks/opencode-db-path-CRgzBK5U.js +0 -402
  174. package/build/server/chunks/opencode-db-path-CRgzBK5U.js.map +0 -1
  175. package/build/server/chunks/pty-manager-aFpChJah.js.map +0 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Provider registry — the single place that knows every AI-agent provider.
3
+ *
4
+ * Inspired by wesm/agentsview's AgentDef registry: instead of branching on
5
+ * `command === 'claude'` across ~14 call sites, the session API, the connect
6
+ * route, and the terminal allowlist all derive from this list. Adding a
7
+ * provider is one entry here (+ its reader) rather than edits everywhere.
8
+ *
9
+ * Detection (process-detector) and live watching (server.ts adapter) stay
10
+ * provider-specific because their mechanisms differ too much to unify cheaply.
11
+ */
12
+
13
+ import type { ConversationMessage, ProjectGroup, ProviderDef } from '$lib/types';
14
+
15
+ import { getAmpConversation, listAmpProjects } from './amp-reader';
16
+ import { getCodexConversation, listCodexProjects } from './codex-reader';
17
+ import { getCopilotConversation, listCopilotProjects } from './copilot-reader';
18
+ import { getCursorConversation, listCursorProjects } from './cursor-reader';
19
+ import { getGeminiConversation, listGeminiProjects } from './gemini-reader';
20
+ import { getSessionConversation, listProjectsWithSessions } from './jsonl-reader';
21
+ import { getOpenCodeConversation, listOpenCodeProjects } from './opencode-reader';
22
+ import { getQwenConversation, listQwenProjects } from './qwen-reader';
23
+
24
+ /**
25
+ * Every AI-agent provider, in merge order. `claude-code` MUST stay first: its
26
+ * projects seed the merge map so the canonical (decoded) project path/name wins
27
+ * over other providers' guesses.
28
+ */
29
+ export const PROVIDERS: ProviderDef[] = [
30
+ {
31
+ command: 'claude',
32
+ getConversation: (id, offset, limit) => getSessionConversation(id, offset, limit),
33
+ isAI: true,
34
+ label: 'Claude Code',
35
+ listProjects: listProjectsWithSessions,
36
+ resumeArgs: (id) => ['--resume', id],
37
+ source: 'claude-code',
38
+ },
39
+ {
40
+ command: 'opencode',
41
+ getConversation: getOpenCodeConversation,
42
+ isAI: true,
43
+ label: 'OpenCode',
44
+ listProjects: listOpenCodeProjects,
45
+ nameSuffix: ' (OpenCode)',
46
+ resumeArgs: (id) => ['--session', id],
47
+ source: 'opencode',
48
+ },
49
+ {
50
+ command: 'codex',
51
+ getConversation: getCodexConversation,
52
+ isAI: true,
53
+ label: 'Codex',
54
+ listProjects: listCodexProjects,
55
+ resumeArgs: (id) => ['resume', id],
56
+ source: 'codex',
57
+ },
58
+ {
59
+ command: 'gemini',
60
+ getConversation: getGeminiConversation,
61
+ isAI: true,
62
+ label: 'Gemini',
63
+ listProjects: listGeminiProjects,
64
+ resumeArgs: () => [], // Gemini CLI has no session-resume flag
65
+ source: 'gemini',
66
+ },
67
+ {
68
+ command: 'qwen',
69
+ getConversation: getQwenConversation,
70
+ isAI: true,
71
+ label: 'Qwen',
72
+ listProjects: listQwenProjects,
73
+ resumeArgs: () => [],
74
+ source: 'qwen',
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
+ },
103
+ ];
104
+
105
+ /** AI-agent binary names (for AI_COMMANDS-style checks). */
106
+ export const AI_COMMANDS: string[] = PROVIDERS.filter((p) => p.isAI).map((p) => p.command);
107
+
108
+ /** All provider binary names (for the terminal allowlist + connect validation). */
109
+ export const PROVIDER_COMMANDS: string[] = PROVIDERS.map((p) => p.command);
110
+
111
+ /** Resolve a session's conversation across providers (Claude first, with its project dir). */
112
+ export function getProviderConversation(
113
+ sessionId: string,
114
+ offset: number,
115
+ limit: number,
116
+ claudeProjectDir?: string
117
+ ): ConversationMessage[] {
118
+ const claude = getSessionConversation(sessionId, offset, limit, claudeProjectDir);
119
+ if (claude.length > 0) {
120
+ return claude;
121
+ }
122
+ for (const provider of PROVIDERS) {
123
+ if (provider.source === 'claude-code') {
124
+ continue;
125
+ }
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
133
+ }
134
+ }
135
+ return [];
136
+ }
137
+
138
+ /** Merge every provider's projects, deduplicating by absolute path. */
139
+ export function listAllProviderProjects(): ProjectGroup[] {
140
+ const byPath = new Map<string, ProjectGroup>();
141
+ for (const provider of PROVIDERS) {
142
+ let groups: ProjectGroup[];
143
+ try {
144
+ groups = provider.listProjects();
145
+ } catch {
146
+ continue; // a broken provider must not take down the whole listing
147
+ }
148
+ for (const group of groups) {
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
167
+ }
168
+ }
169
+ }
170
+ return [...byPath.values()].sort(
171
+ (a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
172
+ );
173
+ }
174
+
175
+ /** Resume args for a launched command (e.g. `codex resume <id>`). */
176
+ export function resumeArgsForCommand(command: string, sessionId: string): string[] {
177
+ return PROVIDERS.find((p) => p.command === command)?.resumeArgs(sessionId) ?? [];
178
+ }
@@ -15,6 +15,7 @@ import { watch as chokidarWatch } from 'chokidar';
15
15
  import * as fs from 'fs';
16
16
 
17
17
  import { CodexStreamParser, parseCodexRollout } from '../sessions/codex-parser';
18
+ import { readBoundedRolloutText } from '../sessions/codex-reader';
18
19
 
19
20
  /** Flush the open run after this many ms without a write. */
20
21
  const IDLE_FLUSH_MS = 1500;
@@ -28,7 +29,9 @@ class CodexWatcher {
28
29
  return [];
29
30
  }
30
31
  try {
31
- return parseCodexRollout(fs.readFileSync(filePath, 'utf-8')).messages;
32
+ // Bounded read — rollout files can be hundreds of MB; this is called on
33
+ // every WS session connection (mirrors getCodexConversation).
34
+ return parseCodexRollout(readBoundedRolloutText(filePath)).messages;
32
35
  } catch (error) {
33
36
  console.error(`[codex-watcher] Failed to read history for ${filePath}:`, error);
34
37
  return [];
@@ -0,0 +1,163 @@
1
+ /**
2
+ * GenericSessionWatcher — live tailing for the five read-only providers
3
+ * (cursor, copilot, qwen, gemini, amp).
4
+ *
5
+ * The byte-offset watchers (claude/codex) and the SQLite poller (opencode)
6
+ * each understand one wire format. These five providers store sessions in
7
+ * heterogeneous shapes — per-turn JSONL for some, a single rewritten JSON
8
+ * document for others — so a byte-incremental reader would need five bespoke
9
+ * parsers. Instead this watcher re-parses the whole file on each change via
10
+ * the shared `parseReadOnlyProviderFile` dispatch and emits only messages
11
+ * whose ID it has not delivered before. That makes it correct for both
12
+ * append-only and whole-document-rewrite formats with one code path.
13
+ *
14
+ * The public surface (getHistory + ref-counted subscribe) matches
15
+ * SessionWatcherLike, so the server's session-watcher adapter can route to it
16
+ * by path without any handler changes.
17
+ */
18
+
19
+ import type { ConversationMessage, GenericWatchedFile, OnNewEntries } from '$lib/types';
20
+
21
+ import { watch as chokidarWatch } from 'chokidar';
22
+
23
+ import { parseReadOnlyProviderFile } from '../sessions/provider-paths';
24
+
25
+ class GenericSessionWatcher {
26
+ private watched = new Map<string, GenericWatchedFile>();
27
+
28
+ /** Re-read the whole file and return the full parsed conversation. */
29
+ getHistory(filePath: string): ConversationMessage[] {
30
+ return parseReadOnlyProviderFile(filePath);
31
+ }
32
+
33
+ /** Stop watching a single file and release its chokidar handle. */
34
+ stop(filePath: string): void {
35
+ const watched = this.watched.get(filePath);
36
+ if (!watched) {
37
+ return;
38
+ }
39
+ void watched.watcher.close();
40
+ this.watched.delete(filePath);
41
+ console.log(`[generic-watcher] Stopped watching: ${filePath}`);
42
+ }
43
+
44
+ /** Stop watching every file. */
45
+ stopAll(): void {
46
+ for (const [filePath] of this.watched) {
47
+ this.stop(filePath);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Subscribe to new messages for a file. Adds the callback to an existing
53
+ * watcher when one is present (ref-counted), otherwise starts a chokidar
54
+ * watch. Returns an unsubscribe function that tears the watcher down once
55
+ * the last subscriber leaves — identical lifecycle to SessionWatcher.
56
+ */
57
+ subscribe(filePath: string, onNewEntries: OnNewEntries): () => void {
58
+ const existing = this.watched.get(filePath);
59
+ if (existing) {
60
+ existing.callbacks.add(onNewEntries);
61
+ return () => {
62
+ this.removeCallback(filePath, onNewEntries);
63
+ };
64
+ }
65
+
66
+ // Seed the emitted set with everything already in the file so the first
67
+ // change only surfaces genuinely new messages — history is sent separately
68
+ // via getHistory, mirroring the byte watchers' initial-offset behaviour.
69
+ const emittedMessageIds = new Set<string>();
70
+ try {
71
+ for (const msg of parseReadOnlyProviderFile(filePath)) {
72
+ emittedMessageIds.add(msg.id);
73
+ }
74
+ } catch (error) {
75
+ // Degrade gracefully: an unreadable/racy file at subscribe time must not
76
+ // prevent watching — the first change will just re-surface what's there.
77
+ console.error(`[generic-watcher] Failed to seed ${filePath}:`, error);
78
+ }
79
+
80
+ const watcher = chokidarWatch(filePath, {
81
+ awaitWriteFinish: { pollInterval: 100, stabilityThreshold: 200 },
82
+ ignoreInitial: true,
83
+ usePolling: false,
84
+ });
85
+
86
+ const watched: GenericWatchedFile = {
87
+ callbacks: new Set([onNewEntries]),
88
+ emittedMessageIds,
89
+ filePath,
90
+ watcher,
91
+ };
92
+
93
+ const onChange = (): void => {
94
+ this.readNew(watched);
95
+ };
96
+ watcher.on('add', onChange);
97
+ watcher.on('change', onChange);
98
+ watcher.on('error', (error) => {
99
+ console.error(`[generic-watcher] Error watching ${filePath}:`, error);
100
+ });
101
+
102
+ this.watched.set(filePath, watched);
103
+ console.log(`[generic-watcher] Watching: ${filePath} (seeded ${emittedMessageIds.size} msgs)`);
104
+
105
+ return () => {
106
+ this.removeCallback(filePath, onNewEntries);
107
+ };
108
+ }
109
+
110
+ /** Legacy fire-and-forget API matching SessionWatcher.watch(). */
111
+ watch(filePath: string, onNewEntries: OnNewEntries): void {
112
+ this.subscribe(filePath, onNewEntries);
113
+ }
114
+
115
+ /**
116
+ * Re-parse the file and deliver any messages whose ID has not been emitted.
117
+ * If a rewrite drops the count below what we have seen (truncation/reset),
118
+ * the new IDs simply won't match, so nothing spurious is sent.
119
+ */
120
+ private readNew(watched: GenericWatchedFile): void {
121
+ let messages: ConversationMessage[];
122
+ try {
123
+ messages = parseReadOnlyProviderFile(watched.filePath);
124
+ } catch (error) {
125
+ console.error(`[generic-watcher] Failed to re-read ${watched.filePath}:`, error);
126
+ return;
127
+ }
128
+
129
+ const fresh = messages.filter((msg) => !watched.emittedMessageIds.has(msg.id));
130
+ if (fresh.length === 0) {
131
+ return;
132
+ }
133
+ for (const msg of fresh) {
134
+ watched.emittedMessageIds.add(msg.id);
135
+ }
136
+ for (const cb of watched.callbacks) {
137
+ try {
138
+ cb(fresh);
139
+ } catch (cbError) {
140
+ console.error('[generic-watcher] Callback error:', cbError);
141
+ }
142
+ }
143
+ }
144
+
145
+ /** Remove one callback; stop watching when none remain. */
146
+ private removeCallback(filePath: string, onNewEntries: OnNewEntries): void {
147
+ const watched = this.watched.get(filePath);
148
+ if (!watched) {
149
+ return;
150
+ }
151
+ watched.callbacks.delete(onNewEntries);
152
+ if (watched.callbacks.size === 0) {
153
+ this.stop(filePath);
154
+ }
155
+ }
156
+ }
157
+
158
+ // Single shared instance across module loaders (matches sessionWatcher).
159
+ const GW_GLOBAL_KEY = '__shooter_generic_session_watcher';
160
+ export const genericSessionWatcher: GenericSessionWatcher =
161
+ ((globalThis as Record<string, unknown>)[GW_GLOBAL_KEY] as GenericSessionWatcher) ||
162
+ new GenericSessionWatcher();
163
+ (globalThis as Record<string, unknown>)[GW_GLOBAL_KEY] = genericSessionWatcher;
@@ -13,6 +13,10 @@ import path from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
 
15
15
  import { findCodexRolloutForCwd } from '../sessions/codex-reader';
16
+ import {
17
+ discoverReadOnlyProviderSessionFile,
18
+ readOnlySourceForCommand,
19
+ } from '../sessions/provider-paths';
16
20
  import { broadcastEvent } from '../ws/server.js';
17
21
  import { HolderClient } from './holder-client';
18
22
  import { openCodeWatcher } from './opencode-watcher';
@@ -1005,6 +1009,53 @@ class PtyManager {
1005
1009
  5 * 60 * 1000
1006
1010
  );
1007
1011
  }
1012
+
1013
+ // For the read-only providers (cursor/copilot/qwen/gemini/amp): poll their
1014
+ // store for a session started after launch and matching this cwd, then set
1015
+ // sessionFile to that path. The WS adapter routes it to the generic watcher
1016
+ // by its provider-root path, giving the same live-tail the JSONL watchers
1017
+ // provide for Claude/Codex.
1018
+ const readOnlySource = readOnlySourceForCommand(command);
1019
+ if (readOnlySource) {
1020
+ const launchTime = terminal.createdAt.getTime();
1021
+ terminal.pollTimer = setInterval(() => {
1022
+ if (terminal.status === 'exited' || terminal.sessionFile) {
1023
+ if (terminal.pollTimer) {
1024
+ clearInterval(terminal.pollTimer);
1025
+ terminal.pollTimer = null;
1026
+ }
1027
+ return;
1028
+ }
1029
+ try {
1030
+ const file = discoverReadOnlyProviderSessionFile(
1031
+ readOnlySource,
1032
+ cwd,
1033
+ launchTime,
1034
+ Date.now()
1035
+ );
1036
+ if (file) {
1037
+ terminal.sessionFile = file;
1038
+ if (terminal.pollTimer) {
1039
+ clearInterval(terminal.pollTimer);
1040
+ terminal.pollTimer = null;
1041
+ }
1042
+ terminalStore.update(id, { sessionFile: file });
1043
+ }
1044
+ } catch {
1045
+ // ignore filesystem errors
1046
+ }
1047
+ }, 2000);
1048
+
1049
+ setTimeout(
1050
+ () => {
1051
+ if (terminal.pollTimer) {
1052
+ clearInterval(terminal.pollTimer);
1053
+ terminal.pollTimer = null;
1054
+ }
1055
+ },
1056
+ 5 * 60 * 1000
1057
+ );
1058
+ }
1008
1059
  }
1009
1060
 
1010
1061
  /** Wire up all HolderClient callbacks (activity, CWD, output, exit, disconnect). */
@@ -22,6 +22,8 @@ import type { WebSocket } from 'ws';
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
 
25
+ import { findCodexRolloutById } from '../sessions/codex-reader';
26
+
25
27
  // ── Module-level references ──────────────────────────────────────────
26
28
 
27
29
  let _ptyManager: null | PtyManagerLike = null;
@@ -171,34 +173,36 @@ function conversationToLive(msg: ConversationMessage): ServerMessage[] {
171
173
  * Returns the absolute path if found, or null.
172
174
  */
173
175
  function findJsonlFileForSession(sessionId: string): null | string {
174
- const claudeProjectsDir = path.join(process.env.HOME || '', '.claude', 'projects');
175
-
176
- if (!fs.existsSync(claudeProjectsDir)) {
176
+ // Reject anything that could traverse out of the session directories before
177
+ // it is interpolated into a filesystem path.
178
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
177
179
  return null;
178
180
  }
179
181
 
180
- try {
181
- const projectDirs = fs.readdirSync(claudeProjectsDir);
182
- for (const dir of projectDirs) {
183
- const fullDir = path.join(claudeProjectsDir, dir);
184
- try {
185
- if (!fs.statSync(fullDir).isDirectory()) {
182
+ const claudeProjectsDir = path.join(process.env.HOME || '', '.claude', 'projects');
183
+ if (fs.existsSync(claudeProjectsDir)) {
184
+ try {
185
+ for (const dir of fs.readdirSync(claudeProjectsDir)) {
186
+ const fullDir = path.join(claudeProjectsDir, dir);
187
+ try {
188
+ if (!fs.statSync(fullDir).isDirectory()) {
189
+ continue;
190
+ }
191
+ } catch {
186
192
  continue;
187
193
  }
188
- } catch {
189
- continue;
190
- }
191
-
192
- const jsonlPath = path.join(fullDir, `${sessionId}.jsonl`);
193
- if (fs.existsSync(jsonlPath)) {
194
- return jsonlPath;
194
+ const jsonlPath = path.join(fullDir, `${sessionId}.jsonl`);
195
+ if (fs.existsSync(jsonlPath)) {
196
+ return jsonlPath;
197
+ }
195
198
  }
199
+ } catch {
200
+ // Ignore filesystem errors
196
201
  }
197
- } catch {
198
- // Ignore filesystem errors
199
202
  }
200
203
 
201
- return null;
204
+ // Fall back to an external Codex session: ~/.codex/sessions/**/rollout-*-<id>.jsonl
205
+ return findCodexRolloutById(sessionId);
202
206
  }
203
207
 
204
208
  // ── Helpers ──────────────────────────────────────────────────────────
@@ -218,7 +222,17 @@ function findTerminalBySessionUuid(uuid: string): ManagedTerminal | undefined {
218
222
  // module-level _ptyManagerFull reference if available.
219
223
  if (_ptyManagerFull) {
220
224
  for (const t of _ptyManagerFull.list()) {
221
- if (t.sessionFile?.includes(`${uuid}.jsonl`)) {
225
+ // Match across every provider's file naming so a running non-Claude
226
+ // agent can be reached (and replied to) by its session UUID:
227
+ // claude/cursor/qwen: <uuid>.jsonl ; codex: -<uuid>.jsonl ;
228
+ // copilot: <uuid>.jsonl OR <uuid>/events.jsonl ; amp: T-<uuid>.json ;
229
+ // opencode: bare session id.
230
+ if (
231
+ t.sessionFile?.includes(`${uuid}.jsonl`) ||
232
+ t.sessionFile?.endsWith(`/${uuid}/events.jsonl`) ||
233
+ t.sessionFile?.endsWith(`/T-${uuid}.json`) ||
234
+ t.openCodeSessionId === uuid
235
+ ) {
222
236
  return _ptyManager.getTerminal(t.id);
223
237
  }
224
238
  }
package/src/lib/theme.css CHANGED
@@ -526,6 +526,38 @@
526
526
  --pill-cursor: inherit;
527
527
  }
528
528
 
529
+ /* Additional providers reuse the existing colour families (cosmetic only). */
530
+ .pill-source-qwen {
531
+ --pill-background: var(--ds-red-100);
532
+ --pill-color: var(--ds-red-900);
533
+ --pill-font-size: 10px;
534
+ --pill-padding: 2px 8px;
535
+ --pill-hover-background: var(--ds-red-100);
536
+ --pill-hover-color: var(--ds-red-900);
537
+ --pill-cursor: inherit;
538
+ }
539
+
540
+ .pill-source-cursor {
541
+ --pill-background: var(--ds-blue-100);
542
+ --pill-color: var(--ds-blue-900);
543
+ --pill-font-size: 10px;
544
+ --pill-padding: 2px 8px;
545
+ --pill-hover-background: var(--ds-blue-100);
546
+ --pill-hover-color: var(--ds-blue-900);
547
+ --pill-cursor: inherit;
548
+ }
549
+
550
+ .pill-source-copilot,
551
+ .pill-source-amp {
552
+ --pill-background: var(--ds-amber-100);
553
+ --pill-color: var(--ds-amber-900);
554
+ --pill-font-size: 10px;
555
+ --pill-padding: 2px 8px;
556
+ --pill-hover-background: var(--ds-amber-100);
557
+ --pill-hover-color: var(--ds-amber-900);
558
+ --pill-cursor: inherit;
559
+ }
560
+
529
561
  /* ===== Icon Sizes ===== */
530
562
  .icon-14 {
531
563
  --icon-width: 14px;
@@ -0,0 +1,100 @@
1
+ // Gemini CLI session types (hand-written: the on-disk formats are
2
+ // provider-specific and not worth round-tripping through the YAML codegen).
3
+ // Gemini CLI stores user messages in ~/.gemini/tmp/<projectHash>/logs.json
4
+ // and full conversation records in
5
+ // ~/.gemini/tmp/<projectHash>/chats/session-*.json (newer versions only).
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // logs.json — user-messages-only format (all versions)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** Union of all part shapes that appear in a ConversationRecord message. */
12
+ export type GeminiContentPart = GeminiFunctionCallPart | GeminiTextPart | GeminiThoughtPart;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // chats/session-*.json — full ConversationRecord (newer versions)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Full conversation record stored in chats/session-*.json. */
19
+ export interface GeminiConversationRecord {
20
+ directories?: string[];
21
+ kind?: 'main' | 'subagent';
22
+ lastUpdated: string;
23
+ messages: GeminiMessageRecord[];
24
+ projectHash: string;
25
+ sessionId: string;
26
+ startTime: string;
27
+ summary?: string;
28
+ }
29
+
30
+ /** Inline function-call part from the Google GenAI SDK. */
31
+ export interface GeminiFunctionCallPart {
32
+ functionCall: {
33
+ args: Record<string, unknown>;
34
+ id?: string;
35
+ name: string;
36
+ };
37
+ }
38
+
39
+ /** A single entry in ~/.gemini/tmp/<projectHash>/logs.json. */
40
+ export interface GeminiLogEntry {
41
+ message: string;
42
+ messageId: number;
43
+ sessionId: string;
44
+ timestamp: string;
45
+ type: 'user';
46
+ }
47
+
48
+ /** A single message record in a full ConversationRecord. */
49
+ export type GeminiMessageRecord =
50
+ | {
51
+ content: GeminiContentPart[] | string;
52
+ id: string;
53
+ thoughts?: GeminiThoughtSummary[];
54
+ timestamp: string;
55
+ toolCalls?: GeminiToolCallRecord[];
56
+ type: 'gemini';
57
+ }
58
+ | {
59
+ content: GeminiContentPart[] | string;
60
+ id: string;
61
+ timestamp: string;
62
+ type: 'error' | 'info' | 'user' | 'warning';
63
+ };
64
+
65
+ /** Contents of ~/.gemini/projects.json (present only in newer gemini-cli). */
66
+ export type GeminiProjectsJson = Record<string, string>;
67
+
68
+ /** Plain-text content part from the Google GenAI SDK. */
69
+ export interface GeminiTextPart {
70
+ text: string;
71
+ thought?: false;
72
+ }
73
+
74
+ /** Inline thinking/reasoning part from the Google GenAI SDK. */
75
+ export interface GeminiThoughtPart {
76
+ text: string;
77
+ thought: true;
78
+ }
79
+
80
+ /** A thought-summary entry attached to a 'gemini'-type message. */
81
+ export interface GeminiThoughtSummary {
82
+ summary?: string;
83
+ timestamp: string;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // projects.json — project slug → absolute path registry (newer versions)
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /** A single tool-call record attached to a 'gemini'-type message. */
91
+ export interface GeminiToolCallRecord {
92
+ args: Record<string, unknown>;
93
+ description?: string;
94
+ displayName?: string;
95
+ id: string;
96
+ name: string;
97
+ result?: unknown;
98
+ status: 'cancelled' | 'error' | 'pending' | 'success';
99
+ timestamp: string;
100
+ }
@@ -14,7 +14,15 @@ import {
14
14
  * @type { SessionSource }
15
15
  * @description Source tool that produced the session
16
16
  */
17
- export type SessionSource = 'claude-code' | 'opencode' | 'codex' | 'gemini';
17
+ export type SessionSource =
18
+ | 'claude-code'
19
+ | 'opencode'
20
+ | 'codex'
21
+ | 'gemini'
22
+ | 'qwen'
23
+ | 'cursor'
24
+ | 'copilot'
25
+ | 'amp';
18
26
 
19
27
  export function decodeSessionSource(rawInput: unknown): SessionSource | null {
20
28
  switch (rawInput) {
@@ -22,6 +30,10 @@ export function decodeSessionSource(rawInput: unknown): SessionSource | null {
22
30
  case 'opencode':
23
31
  case 'codex':
24
32
  case 'gemini':
33
+ case 'qwen':
34
+ case 'cursor':
35
+ case 'copilot':
36
+ case 'amp':
25
37
  return rawInput;
26
38
  }
27
39
  return null;
@@ -33,6 +45,10 @@ export function _decodeSessionSource(rawInput: unknown): SessionSource | undefin
33
45
  case 'opencode':
34
46
  case 'codex':
35
47
  case 'gemini':
48
+ case 'qwen':
49
+ case 'cursor':
50
+ case 'copilot':
51
+ case 'amp':
36
52
  return rawInput;
37
53
  }
38
54
  return;