@scrylog/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +46 -7
  2. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  3. package/dist/assets/highlights-ghv9g403.scm +205 -0
  4. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  5. package/dist/assets/highlights-r812a2qc.scm +150 -0
  6. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  7. package/dist/assets/injections-73j83es3.scm +27 -0
  8. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  9. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  10. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/dist/cli/assets/providers/SOURCES.md +21 -0
  16. package/dist/cli/assets/providers/claude-terminal.png +0 -0
  17. package/dist/cli/assets/providers/claude-terminal.svg +62 -0
  18. package/dist/cli/assets/providers/claude.png +0 -0
  19. package/dist/cli/assets/providers/copilot.png +0 -0
  20. package/dist/cli/assets/providers/copilot.svg +44 -0
  21. package/dist/cli/assets/providers/openai-terminal.png +0 -0
  22. package/dist/cli/assets/providers/openai-terminal.svg +62 -0
  23. package/dist/cli/assets/providers/openai.png +0 -0
  24. package/dist/cli/assets/providers/opencode-terminal.png +0 -0
  25. package/dist/cli/assets/providers/opencode-terminal.svg +62 -0
  26. package/dist/cli/assets/providers/opencode.png +0 -0
  27. package/dist/cli/assets/providers/shell.png +0 -0
  28. package/dist/cli/assets/providers/shell.svg +23 -0
  29. package/dist/cli/assets/providers/unknown.png +0 -0
  30. package/dist/cli/assets/providers/unknown.svg +23 -0
  31. package/dist/cli/index.js +499 -0
  32. package/dist/cli/index.js.map +61 -0
  33. package/dist/daemon/index.js +670 -0
  34. package/dist/daemon/index.js.map +1475 -0
  35. package/dist/hooks/claude.js +6 -0
  36. package/dist/hooks/claude.js.map +11 -0
  37. package/dist/hooks/codex.js +10 -0
  38. package/dist/hooks/codex.js.map +11 -0
  39. package/dist/mcp/index.js +100 -0
  40. package/dist/mcp/index.js.map +231 -0
  41. package/dist/oc/index.js +21 -0
  42. package/dist/oc/index.js.map +10 -0
  43. package/package.json +26 -20
  44. package/src/postinstall.cjs +11 -0
  45. package/src/api/docs/sessions.keys.ts +0 -6
  46. package/src/api/docs/sessions.ts +0 -81
  47. package/src/api/docs/system.keys.ts +0 -5
  48. package/src/api/docs/system.ts +0 -66
  49. package/src/api/docs/usage.keys.ts +0 -3
  50. package/src/api/docs/usage.ts +0 -24
  51. package/src/api/docs/webhooks.keys.ts +0 -5
  52. package/src/api/docs/webhooks.ts +0 -78
  53. package/src/api/index.ts +0 -18
  54. package/src/api/lib/auth.ts +0 -43
  55. package/src/api/lib/errors.ts +0 -28
  56. package/src/api/lib/route.ts +0 -18
  57. package/src/api/models/claude.ts +0 -4
  58. package/src/api/models/codex.ts +0 -4
  59. package/src/api/models/opencode.ts +0 -11
  60. package/src/api/routes/sessions.ts +0 -47
  61. package/src/api/routes/system.ts +0 -103
  62. package/src/api/routes/usage.ts +0 -15
  63. package/src/api/routes/webhooks/claude.ts +0 -141
  64. package/src/api/routes/webhooks/codex.ts +0 -175
  65. package/src/api/routes/webhooks/opencode.ts +0 -111
  66. package/src/api/routes.ts +0 -93
  67. package/src/api/schemas/common.ts +0 -21
  68. package/src/api/schemas/sessions.ts +0 -72
  69. package/src/api/schemas/usage.ts +0 -50
  70. package/src/api/schemas/webhooks/claude.ts +0 -32
  71. package/src/api/schemas/webhooks/codex.ts +0 -34
  72. package/src/api/schemas/webhooks/opencode.ts +0 -29
  73. package/src/api/services/claude-session.ts +0 -9
  74. package/src/api/services/codex-session.ts +0 -8
  75. package/src/api/services/opencode-session.ts +0 -73
  76. package/src/classifier.ts +0 -206
  77. package/src/cli/commands.ts +0 -632
  78. package/src/cli/prompt.ts +0 -44
  79. package/src/daemon/cli.ts +0 -27
  80. package/src/daemon/collectors/claude/README.md +0 -108
  81. package/src/daemon/collectors/claude/collector.ts +0 -423
  82. package/src/daemon/collectors/claude/discovery.ts +0 -148
  83. package/src/daemon/collectors/claude/hooks.ts +0 -181
  84. package/src/daemon/collectors/claude/index.ts +0 -8
  85. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  86. package/src/daemon/collectors/claude/transcript.ts +0 -246
  87. package/src/daemon/collectors/codex/README.md +0 -147
  88. package/src/daemon/collectors/codex/collector.ts +0 -661
  89. package/src/daemon/collectors/codex/discovery.ts +0 -104
  90. package/src/daemon/collectors/codex/hooks.ts +0 -179
  91. package/src/daemon/collectors/codex/index.ts +0 -7
  92. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  93. package/src/daemon/collectors/codex/transcript.ts +0 -299
  94. package/src/daemon/collectors/git.ts +0 -295
  95. package/src/daemon/collectors/opencode/README.md +0 -106
  96. package/src/daemon/collectors/opencode/collector.ts +0 -545
  97. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  98. package/src/daemon/collectors/opencode/index.ts +0 -5
  99. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  100. package/src/daemon/collectors/opencode-db.ts +0 -282
  101. package/src/daemon/collectors/process.ts +0 -201
  102. package/src/daemon/collectors/tmux/README.md +0 -128
  103. package/src/daemon/collectors/tmux/collector.ts +0 -472
  104. package/src/daemon/collectors/tmux/enrichment.ts +0 -241
  105. package/src/daemon/collectors/tmux/index.ts +0 -4
  106. package/src/daemon/collectors/tmux/parse.ts +0 -128
  107. package/src/daemon/collectors/usage.ts +0 -193
  108. package/src/daemon/index.ts +0 -207
  109. package/src/daemon/scheduler.ts +0 -86
  110. package/src/daemon/store.ts +0 -588
  111. package/src/detect.ts +0 -143
  112. package/src/hooks/claude.ts +0 -596
  113. package/src/hooks/codex.ts +0 -517
  114. package/src/legacy/classifier.ts +0 -215
  115. package/src/legacy/detect.ts +0 -143
  116. package/src/mcp/client-server.ts +0 -21
  117. package/src/mcp/http-handler.ts +0 -126
  118. package/src/mcp/index.ts +0 -61
  119. package/src/mcp/resources/config-current.ts +0 -19
  120. package/src/mcp/resources/index.ts +0 -11
  121. package/src/mcp/resources/sessions-active.ts +0 -19
  122. package/src/mcp/resources/sessions-all.ts +0 -19
  123. package/src/mcp/schemas/sessions.ts +0 -3
  124. package/src/mcp/server.ts +0 -17
  125. package/src/mcp/services/api-adapter.ts +0 -48
  126. package/src/mcp/services/data-source.ts +0 -22
  127. package/src/mcp/services/store-adapter.ts +0 -40
  128. package/src/mcp/tools/get-active-sessions.ts +0 -16
  129. package/src/mcp/tools/get-session-children.ts +0 -24
  130. package/src/mcp/tools/get-session-messages.ts +0 -24
  131. package/src/mcp/tools/get-session.ts +0 -23
  132. package/src/mcp/tools/index.ts +0 -15
  133. package/src/mcp/tools/list-sessions.ts +0 -16
  134. package/src/neo-blessed.d.ts +0 -4
  135. package/src/oc/index.ts +0 -302
  136. package/src/service/manager.ts +0 -284
  137. package/src/shared/config.ts +0 -478
  138. package/src/shared/ipc.ts +0 -80
  139. package/src/shared/logger.ts +0 -248
  140. package/src/shared/paths.ts +0 -73
  141. package/src/shared/platform.ts +0 -110
  142. package/src/shared/session-view.ts +0 -241
  143. package/src/shared/types.ts +0 -299
  144. package/src/shared/utils.ts +0 -339
  145. package/src/tui/app.ts +0 -920
  146. package/src/tui/client.ts +0 -149
  147. package/src/tui/index.ts +0 -217
  148. package/src/types.ts +0 -113
  149. package/src/usage/README.md +0 -323
  150. package/src/usage/api.ts +0 -99
  151. package/src/usage/client.ts +0 -3
  152. package/src/usage/clients/anthropic-oauth-usage.ts +0 -178
  153. package/src/usage/clients/anthropic-web-usage.ts +0 -245
  154. package/src/usage/clients/claude-cli-probe.ts +0 -187
  155. package/src/usage/clients/codex-oauth-usage.ts +0 -113
  156. package/src/usage/clients/opencode-go-usage.ts +0 -441
  157. package/src/usage/index.ts +0 -20
  158. package/src/usage/mappers.ts +0 -273
  159. package/src/usage/planner.ts +0 -54
  160. package/src/usage/providers/claude.ts +0 -113
  161. package/src/usage/providers/codex.ts +0 -34
  162. package/src/usage/providers/opencode-go.ts +0 -37
  163. package/src/usage/resolver.ts +0 -4
  164. package/src/usage/resolvers/browser-cookies.ts +0 -470
  165. package/src/usage/resolvers/claude-auth.ts +0 -108
  166. package/src/usage/resolvers/claude-cli.ts +0 -24
  167. package/src/usage/resolvers/claude-desktop-auth.ts +0 -160
  168. package/src/usage/resolvers/claude-web.ts +0 -16
  169. package/src/usage/resolvers/codex-auth.ts +0 -117
  170. package/src/usage/resolvers/opencode-go-web.ts +0 -14
  171. package/src/usage/types.ts +0 -190
@@ -1,472 +0,0 @@
1
- import type { SessionModel } from "../../../shared/types";
2
- import type { SnapshotHistory } from "../../../shared/types";
3
- import type { ScrylogConfig } from "../../../shared/config";
4
- import type { DaemonStore } from "../../store";
5
- import type { Collector } from "../../scheduler";
6
- import type { Logger } from "../../../shared/logger";
7
- import { NOOP_LOGGER } from "../../../shared/logger";
8
- import type { ProcessCollector } from "../process";
9
- import { buildProcessTree, findAncestorInSet } from "../process";
10
- import type { GitCollector } from "../git";
11
- import { findOpencodeInTree } from "../opencode/discovery";
12
- import { collectTmuxState, capturePane } from "./parse";
13
- import {
14
- isOpenCodeManagedSession,
15
- isClaudeManagedSession,
16
- isCodexManagedSession,
17
- clearStaleClaudeTmuxBindings,
18
- clearStaleCodexTmuxBindings,
19
- clearStaleOpenCodeTmuxBindings,
20
- managedStableForMs,
21
- detectClaudePermissionPrompt,
22
- detectCodexPermissionPrompt,
23
- isClaudePaneCandidate,
24
- isCodexPaneCandidate,
25
- isOpenCodePaneCandidate,
26
- shouldHideOpenCodePane,
27
- looksLikeAgent,
28
- paneModelToSession,
29
- } from "./enrichment";
30
- import { classifyPane } from "../../../legacy/classifier";
31
-
32
- /**
33
- * Tmux collector: runs on a periodic timer, builds pane models, and writes
34
- * them to the store. Delegates classification to legacy detect/classifier.
35
- */
36
- export class TmuxCollector implements Collector {
37
- name = "tmux";
38
- private timer: ReturnType<typeof setInterval> | null = null;
39
- private history = new Map<string, SnapshotHistory>();
40
- private logger: Logger = NOOP_LOGGER;
41
-
42
- constructor(
43
- private readonly processCollector: ProcessCollector,
44
- private readonly gitCollector: GitCollector,
45
- ) {}
46
-
47
- start(store: DaemonStore, config: ScrylogConfig, logger: Logger): void {
48
- this.logger = logger;
49
- const run = () => {
50
- try {
51
- this.collect(store, config);
52
- store.setCollectorStatus(this.name, { running: true, lastRunAt: Date.now() });
53
- } catch (error) {
54
- const msg = error instanceof Error ? error.message : String(error);
55
- store.setCollectorStatus(this.name, { lastError: msg });
56
- store.reportError(this.name, msg);
57
- }
58
- };
59
-
60
- logger.info("tmux", "Collector started", { event: "collector.started", collector: this.name, intervalMs: config.daemon.pollIntervalMs });
61
- run();
62
- this.timer = setInterval(run, config.daemon.pollIntervalMs);
63
- }
64
-
65
- stop(): void {
66
- if (this.timer) {
67
- clearInterval(this.timer);
68
- this.timer = null;
69
- }
70
- this.logger.info("tmux", "Collector stopped", { event: "collector.stopped", collector: this.name });
71
- }
72
-
73
- private collect(store: DaemonStore, config: ScrylogConfig): void {
74
- const tmux = collectTmuxState(config.daemon.commandTimeoutMs);
75
- store.setTmuxSessions(tmux.sessions);
76
-
77
- if (tmux.errors.length > 0) {
78
- this.logger.warn("tmux", "tmux reported errors", { event: "collector.poll_partial", collector: this.name, errors: tmux.errors });
79
- }
80
-
81
- for (const msg of tmux.errors) {
82
- store.reportError(this.name, msg);
83
- }
84
-
85
- if (tmux.panes.length === 0) {
86
- this.logger.debug("tmux", "No panes found — clearing fallback sessions", { event: "collector.poll_completed", collector: this.name, panes: 0 });
87
- clearStaleOpenCodeTmuxBindings(store, new Set());
88
- // Remove all sessions that came from tmux
89
- const current = store.getSessions();
90
- for (const session of current) {
91
- if (session.source === "tmux" && session.detectionSource === "fallback-detection") {
92
- store.removeSession(session.id);
93
- }
94
- }
95
- return;
96
- }
97
-
98
- this.logger.debug("tmux", "Collected panes", { event: "collector.poll_completed", collector: this.name, panes: tmux.panes.length, sessions: tmux.sessions.length });
99
-
100
- const allProcesses = this.processCollector.processes;
101
- const now = Date.now();
102
-
103
- // Build set of known SDK PIDs for PID-ancestry matching
104
- const sdkPidMap = store.getSdkPids();
105
- const sdkPidSet = new Set(sdkPidMap.keys());
106
-
107
- const newFallbackSessions: SessionModel[] = [];
108
-
109
- for (const pane of tmux.panes) {
110
- const processTree = buildProcessTree(pane.panePid, allProcesses);
111
- const shouldPreview = looksLikeAgent(processTree, pane.currentCommand);
112
- const sample = shouldPreview
113
- ? capturePane(pane.paneId, config.tmux.previewLines, config.daemon.commandTimeoutMs)
114
- : null;
115
-
116
- const result = classifyPane({
117
- pane: {
118
- ...pane,
119
- project: this.gitCollector.resolve(pane.currentPath),
120
- },
121
- processTree,
122
- sample,
123
- history: this.history.get(pane.paneId),
124
- now,
125
- });
126
-
127
- this.history.set(pane.paneId, result.history);
128
-
129
- if (shouldHideOpenCodePane(config.opencode, pane, processTree, result.pane.tool)) {
130
- this.logger.debug("tmux", "Hiding OpenCode pane in SSE mode", {
131
- paneId: pane.paneId,
132
- currentCommand: pane.currentCommand,
133
- });
134
- continue;
135
- }
136
-
137
- // Check if any process in the tree belongs to a known SDK instance
138
- // Walk all PIDs in the process tree and check for ancestor match
139
- const allTreePids = new Set(processTree.map((p) => p.pid));
140
- allTreePids.add(pane.panePid);
141
-
142
- let matchedSdkPid: number | undefined;
143
- for (const pid of allTreePids) {
144
- const ancestor = findAncestorInSet(pid, sdkPidSet, allProcesses);
145
- if (ancestor !== undefined) {
146
- matchedSdkPid = ancestor;
147
- break;
148
- }
149
- }
150
-
151
- // ── M0: plugin-written tmux pane option ──────────────────────────────────
152
- // The scrylog opencode plugin writes @opencode-session to the pane user
153
- // option whenever session.created / session.updated fires. When present,
154
- // this is an exact, authoritative match — skip all other heuristics.
155
- if (pane.opencodeSession) {
156
- const current = store.getSessions();
157
- const pluginSession = current.find(
158
- (s) =>
159
- isOpenCodeManagedSession(s) &&
160
- s.opencode?.sessionId === pane.opencodeSession &&
161
- (!s.legacyPaneId || s.legacyPaneId === pane.paneId),
162
- );
163
- if (pluginSession) {
164
- store.clearTmuxBindingForPane(pane.paneId, pluginSession.id);
165
- const enriched: SessionModel = {
166
- ...pluginSession,
167
- source: "tmux",
168
- project: result.pane.project,
169
- tmux: {
170
- session: pane.sessionName,
171
- window: String(pane.windowIndex),
172
- pane: pane.paneId,
173
- },
174
- legacyPaneId: pane.paneId,
175
- processTree: result.pane.processTree,
176
- processChain: result.pane.processChain,
177
- sample: result.pane.sample,
178
- stableForMs: managedStableForMs(pluginSession),
179
- currentCommand: pane.currentCommand,
180
- currentPath: pane.currentPath,
181
- panePid: pane.panePid,
182
- tty: pane.tty,
183
- paneTitle: pane.title,
184
- };
185
- this.logger.debug("tmux", "Pane matched by plugin pane option — enriching", {
186
- paneId: pane.paneId,
187
- sessionId: pane.opencodeSession,
188
- });
189
- store.upsertSession(enriched);
190
- continue;
191
- }
192
- // Exact session ID is authoritative. If the referenced session is not in
193
- // the store yet, do not attempt any automatic OpenCode pane matching.
194
- this.logger.debug("tmux", "Plugin pane option set but session not found in store yet", {
195
- paneId: pane.paneId,
196
- opencodeSession: pane.opencodeSession,
197
- });
198
- newFallbackSessions.push(paneModelToSession(result.pane));
199
- continue;
200
- }
201
-
202
- // ── M1: hook-written @claude-session pane option ─────────────────────────
203
- // The scrylog-claude-hook binary writes @claude-session to the pane when
204
- // Claude runs inside tmux. This is an exact, authoritative match.
205
- if (pane.claudeSession) {
206
- const current = store.getSessions();
207
- const linkedSession = current.find(
208
- (s) =>
209
- isClaudeManagedSession(s) &&
210
- s.claude?.sessionId === pane.claudeSession &&
211
- (!s.legacyPaneId || s.legacyPaneId === pane.paneId),
212
- );
213
- if (linkedSession) {
214
- // Only apply detectClaudePermissionPrompt for non-hook-managed sessions.
215
- // Hook-managed sessions have authoritative state from the PermissionRequest
216
- // event and must not be overridden by stale tmux screen sampling.
217
- const claudePrompt = linkedSession.detectionSource === "claude-hook"
218
- ? null
219
- : detectClaudePermissionPrompt(result.pane.sample);
220
- store.clearTmuxBindingForPane(pane.paneId, linkedSession.id);
221
- const enriched: SessionModel = {
222
- ...linkedSession,
223
- source: "tmux",
224
- ...(claudePrompt ? {
225
- state: claudePrompt.state,
226
- hasAttention: claudePrompt.hasAttention,
227
- summary: claudePrompt.summary,
228
- } : {}),
229
- project: result.pane.project,
230
- tmux: {
231
- session: pane.sessionName,
232
- window: String(pane.windowIndex),
233
- pane: pane.paneId,
234
- },
235
- legacyPaneId: pane.paneId,
236
- processTree: result.pane.processTree,
237
- processChain: result.pane.processChain,
238
- sample: result.pane.sample,
239
- stableForMs: managedStableForMs(linkedSession),
240
- currentCommand: pane.currentCommand,
241
- currentPath: pane.currentPath,
242
- panePid: pane.panePid,
243
- tty: pane.tty,
244
- paneTitle: pane.title,
245
- };
246
- this.logger.debug("tmux", "Pane matched by @claude-session option — enriching", {
247
- paneId: pane.paneId,
248
- claudeSessionId: pane.claudeSession,
249
- });
250
- store.upsertSession(enriched);
251
- continue;
252
- }
253
- // Check if deliberately excluded (local-command batch run)
254
- if (store.isLocalCommand(pane.claudeSession)) {
255
- this.logger.debug("tmux", "Skipping fallback for local-command session", {
256
- paneId: pane.paneId,
257
- claudeSession: pane.claudeSession,
258
- });
259
- continue;
260
- }
261
- // Authoritative but not yet in store — keep as fallback
262
- this.logger.debug("tmux", "@claude-session set but session not in store yet", {
263
- paneId: pane.paneId,
264
- claudeSession: pane.claudeSession,
265
- });
266
- newFallbackSessions.push(paneModelToSession(result.pane));
267
- continue;
268
- }
269
-
270
- // ── M1.5: hook-written @codex-session pane option ────────────────────────
271
- // The scrylog-codex-hook binary writes @codex-session to the pane when
272
- // Codex runs inside tmux. This is an exact, authoritative match.
273
- if (pane.codexSession) {
274
- const current = store.getSessions();
275
- const linkedSession = current.find(
276
- (s) =>
277
- isCodexManagedSession(s) &&
278
- s.codex?.sessionId === pane.codexSession &&
279
- (!s.legacyPaneId || s.legacyPaneId === pane.paneId),
280
- );
281
- if (linkedSession) {
282
- const codexPrompt = linkedSession.detectionSource === "codex-hook"
283
- ? null
284
- : detectCodexPermissionPrompt(result.pane.sample);
285
- store.clearTmuxBindingForPane(pane.paneId, linkedSession.id);
286
- const enriched: SessionModel = {
287
- ...linkedSession,
288
- source: "tmux",
289
- ...(codexPrompt ? {
290
- state: codexPrompt.state,
291
- hasAttention: codexPrompt.hasAttention,
292
- summary: codexPrompt.summary,
293
- } : {}),
294
- project: result.pane.project,
295
- tmux: {
296
- session: pane.sessionName,
297
- window: String(pane.windowIndex),
298
- pane: pane.paneId,
299
- },
300
- legacyPaneId: pane.paneId,
301
- processTree: result.pane.processTree,
302
- processChain: result.pane.processChain,
303
- sample: result.pane.sample,
304
- stableForMs: managedStableForMs(linkedSession),
305
- currentCommand: pane.currentCommand,
306
- currentPath: pane.currentPath,
307
- panePid: pane.panePid,
308
- tty: pane.tty,
309
- paneTitle: pane.title,
310
- };
311
- this.logger.debug("tmux", "Pane matched by @codex-session option — enriching", {
312
- paneId: pane.paneId,
313
- codexSessionId: pane.codexSession,
314
- });
315
- store.upsertSession(enriched);
316
- continue;
317
- }
318
- // Authoritative but not yet in store — keep as fallback
319
- this.logger.debug("tmux", "@codex-session set but session not in store yet", {
320
- paneId: pane.paneId,
321
- codexSession: pane.codexSession,
322
- });
323
- newFallbackSessions.push(paneModelToSession(result.pane));
324
- continue;
325
- }
326
-
327
- // Claude panes without the pane option: keep as fallback to avoid duplicate
328
- // entries once the hook fires and the pane option is set.
329
- if (isClaudePaneCandidate(processTree, pane.currentCommand)) {
330
- newFallbackSessions.push(paneModelToSession(result.pane));
331
- continue;
332
- }
333
-
334
- // Codex panes without the pane option: keep as fallback.
335
- if (isCodexPaneCandidate(processTree, pane.currentCommand)) {
336
- newFallbackSessions.push(paneModelToSession(result.pane));
337
- continue;
338
- }
339
-
340
- // OpenCode panes inside tmux must only correlate by exact session ID.
341
- // Without the plugin-written pane option, keep a temporary fallback pane
342
- // row and avoid heuristic matching to DB/SDK sessions.
343
- if (isOpenCodePaneCandidate(pane, processTree, result.pane.tool)) {
344
- newFallbackSessions.push(paneModelToSession(result.pane));
345
- continue;
346
- }
347
-
348
- if (matchedSdkPid !== undefined) {
349
- // Find the SDK sessions for this instance and enrich them with tmux context.
350
- const instanceUrl = sdkPidMap.get(matchedSdkPid);
351
- const current = store.getSessions();
352
- const sdkSessionsForInstance = current.filter(
353
- (s) =>
354
- isOpenCodeManagedSession(s) &&
355
- s.opencode?.instanceUrl === instanceUrl &&
356
- !s.legacyPaneId,
357
- );
358
-
359
- for (const sdkSession of sdkSessionsForInstance) {
360
- const enriched: SessionModel = {
361
- ...sdkSession,
362
- // Switch to tmux source so TUI groups it with tmux sessions
363
- source: "tmux",
364
- project: result.pane.project,
365
- tmux: {
366
- session: pane.sessionName,
367
- window: String(pane.windowIndex),
368
- pane: pane.paneId,
369
- },
370
- legacyPaneId: pane.paneId,
371
- // Extended fields from pane analysis
372
- processTree: result.pane.processTree,
373
- processChain: result.pane.processChain,
374
- sample: result.pane.sample,
375
- stableForMs: managedStableForMs(sdkSession),
376
- currentCommand: pane.currentCommand,
377
- currentPath: pane.currentPath,
378
- panePid: pane.panePid,
379
- tty: pane.tty,
380
- paneTitle: pane.title,
381
- };
382
- this.logger.debug("tmux", "Pane covered by SDK — enriching SDK session", {
383
- paneId: pane.paneId,
384
- sdkSessionId: sdkSession.id,
385
- instanceUrl,
386
- });
387
- store.upsertSession(enriched);
388
- }
389
- // Don't create a fallback entry for this pane
390
- this.logger.debug("tmux", "Pane covered by SDK — skipping fallback entry", {
391
- paneId: pane.paneId,
392
- matchedSdkPid,
393
- instanceUrl,
394
- });
395
- } else {
396
- // PID ancestry via sdkPidMap failed (e.g. registerSdkPid not yet called).
397
- // Attempt URL-from-args matching: walk the pane's ancestor chain looking
398
- // for an opencode process and derive the instance URL from its args.
399
- // This is deterministic and has no false positives unlike path matching.
400
- const ancestor = findOpencodeInTree(processTree);
401
- if (ancestor) {
402
- const current = store.getSessions();
403
- const sdkSessionsForInstance = current.filter(
404
- (s) =>
405
- isOpenCodeManagedSession(s) &&
406
- s.opencode?.instanceUrl === ancestor.url &&
407
- !s.legacyPaneId,
408
- );
409
-
410
- if (sdkSessionsForInstance.length > 0) {
411
- for (const sdkSession of sdkSessionsForInstance) {
412
- const enriched: SessionModel = {
413
- ...sdkSession,
414
- source: "tmux",
415
- project: result.pane.project,
416
- tmux: {
417
- session: pane.sessionName,
418
- window: String(pane.windowIndex),
419
- pane: pane.paneId,
420
- },
421
- legacyPaneId: pane.paneId,
422
- processTree: result.pane.processTree,
423
- processChain: result.pane.processChain,
424
- sample: result.pane.sample,
425
- stableForMs: managedStableForMs(sdkSession),
426
- currentCommand: pane.currentCommand,
427
- currentPath: pane.currentPath,
428
- panePid: pane.panePid,
429
- tty: pane.tty,
430
- paneTitle: pane.title,
431
- };
432
- this.logger.debug("tmux", "Pane matched SDK session by URL-from-args — enriching", {
433
- paneId: pane.paneId,
434
- sdkSessionId: sdkSession.id,
435
- instanceUrl: ancestor.url,
436
- });
437
- store.upsertSession(enriched);
438
- }
439
- this.logger.debug("tmux", "Pane covered by URL-from-args match — skipping fallback", {
440
- paneId: pane.paneId,
441
- instanceUrl: ancestor.url,
442
- });
443
- } else {
444
- newFallbackSessions.push(paneModelToSession(result.pane));
445
- }
446
- } else {
447
- newFallbackSessions.push(paneModelToSession(result.pane));
448
- }
449
- }
450
- }
451
-
452
- // Prune history for gone panes
453
- const livePaneIds = new Set(tmux.panes.map((pane) => pane.paneId));
454
- clearStaleOpenCodeTmuxBindings(store, livePaneIds);
455
- clearStaleClaudeTmuxBindings(store, livePaneIds);
456
- clearStaleCodexTmuxBindings(store, livePaneIds);
457
-
458
- for (const paneId of [...this.history.keys()]) {
459
- if (!livePaneIds.has(paneId)) {
460
- this.history.delete(paneId);
461
- }
462
- }
463
-
464
- // Replace only fallback sessions (non-enriched); keep SDK-sourced sessions
465
- const current = store.getSessions();
466
- const nonFallback = current.filter(
467
- (s) => !(s.source === "tmux" && s.detectionSource === "fallback-detection"),
468
- );
469
-
470
- store.setSessions([...nonFallback, ...newFallbackSessions]);
471
- }
472
- }