@scrylog/cli 0.3.0 → 0.3.2

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 (195) hide show
  1. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/assets/highlights-ghv9g403.scm +205 -0
  3. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/assets/highlights-r812a2qc.scm +150 -0
  5. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/assets/injections-73j83es3.scm +27 -0
  7. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  8. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  9. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  10. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  12. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  13. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  14. package/dist/cli/index.js +499 -0
  15. package/dist/cli/index.js.map +61 -0
  16. package/dist/daemon/index.js +670 -0
  17. package/dist/daemon/index.js.map +1475 -0
  18. package/dist/hooks/claude.js +6 -0
  19. package/dist/hooks/claude.js.map +11 -0
  20. package/dist/hooks/codex.js +10 -0
  21. package/dist/hooks/codex.js.map +11 -0
  22. package/dist/mcp/index.js +100 -0
  23. package/dist/mcp/index.js.map +231 -0
  24. package/dist/oc/index.js +21 -0
  25. package/dist/oc/index.js.map +10 -0
  26. package/package.json +16 -13
  27. package/src/api/docs/sessions.keys.ts +0 -6
  28. package/src/api/docs/sessions.ts +0 -82
  29. package/src/api/docs/system.keys.ts +0 -5
  30. package/src/api/docs/system.ts +0 -69
  31. package/src/api/docs/usage.keys.ts +0 -3
  32. package/src/api/docs/usage.ts +0 -24
  33. package/src/api/docs/webhooks.keys.ts +0 -5
  34. package/src/api/docs/webhooks.ts +0 -78
  35. package/src/api/index.ts +0 -18
  36. package/src/api/lib/auth.ts +0 -43
  37. package/src/api/lib/errors.ts +0 -28
  38. package/src/api/lib/route.ts +0 -18
  39. package/src/api/models/claude.ts +0 -4
  40. package/src/api/models/codex.ts +0 -4
  41. package/src/api/models/opencode.ts +0 -11
  42. package/src/api/routes/sessions.ts +0 -51
  43. package/src/api/routes/system.ts +0 -106
  44. package/src/api/routes/usage.ts +0 -15
  45. package/src/api/routes/webhooks/claude.ts +0 -149
  46. package/src/api/routes/webhooks/codex.ts +0 -175
  47. package/src/api/routes/webhooks/opencode.ts +0 -112
  48. package/src/api/routes.ts +0 -93
  49. package/src/api/schemas/common.ts +0 -29
  50. package/src/api/schemas/sessions.ts +0 -86
  51. package/src/api/schemas/usage.ts +0 -51
  52. package/src/api/schemas/webhooks/claude.ts +0 -32
  53. package/src/api/schemas/webhooks/codex.ts +0 -34
  54. package/src/api/schemas/webhooks/opencode.ts +0 -29
  55. package/src/api/services/claude-session.ts +0 -9
  56. package/src/api/services/codex-session.ts +0 -8
  57. package/src/api/services/opencode-session.ts +0 -73
  58. package/src/classifier.ts +0 -206
  59. package/src/cli/commands.ts +0 -714
  60. package/src/cli/index.ts +0 -18
  61. package/src/cli/program.ts +0 -175
  62. package/src/cli/prompt.ts +0 -44
  63. package/src/daemon/cli.ts +0 -27
  64. package/src/daemon/collectors/claude/README.md +0 -108
  65. package/src/daemon/collectors/claude/collector.ts +0 -423
  66. package/src/daemon/collectors/claude/discovery.ts +0 -127
  67. package/src/daemon/collectors/claude/hooks.ts +0 -181
  68. package/src/daemon/collectors/claude/index.ts +0 -8
  69. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  70. package/src/daemon/collectors/claude/transcript.ts +0 -246
  71. package/src/daemon/collectors/codex/README.md +0 -147
  72. package/src/daemon/collectors/codex/collector.ts +0 -661
  73. package/src/daemon/collectors/codex/discovery.ts +0 -104
  74. package/src/daemon/collectors/codex/hooks.ts +0 -179
  75. package/src/daemon/collectors/codex/index.ts +0 -7
  76. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  77. package/src/daemon/collectors/codex/transcript.ts +0 -299
  78. package/src/daemon/collectors/git.ts +0 -353
  79. package/src/daemon/collectors/herdr/collector.ts +0 -128
  80. package/src/daemon/collectors/herdr/index.ts +0 -2
  81. package/src/daemon/collectors/herdr/normalization.ts +0 -111
  82. package/src/daemon/collectors/opencode/README.md +0 -106
  83. package/src/daemon/collectors/opencode/collector.ts +0 -545
  84. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  85. package/src/daemon/collectors/opencode/index.ts +0 -5
  86. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  87. package/src/daemon/collectors/opencode-db.ts +0 -294
  88. package/src/daemon/collectors/process.ts +0 -201
  89. package/src/daemon/collectors/tmux/README.md +0 -128
  90. package/src/daemon/collectors/tmux/collector.ts +0 -492
  91. package/src/daemon/collectors/tmux/enrichment.ts +0 -242
  92. package/src/daemon/collectors/tmux/index.ts +0 -4
  93. package/src/daemon/collectors/tmux/parse.ts +0 -128
  94. package/src/daemon/collectors/usage.ts +0 -259
  95. package/src/daemon/index.ts +0 -209
  96. package/src/daemon/scheduler.ts +0 -86
  97. package/src/daemon/store.ts +0 -630
  98. package/src/detect.ts +0 -143
  99. package/src/hooks/claude.ts +0 -596
  100. package/src/hooks/codex.ts +0 -517
  101. package/src/legacy/classifier.ts +0 -215
  102. package/src/legacy/detect.ts +0 -143
  103. package/src/mcp/client-server.ts +0 -21
  104. package/src/mcp/http-handler.ts +0 -126
  105. package/src/mcp/index.ts +0 -61
  106. package/src/mcp/resources/config-current.ts +0 -19
  107. package/src/mcp/resources/index.ts +0 -11
  108. package/src/mcp/resources/sessions-active.ts +0 -19
  109. package/src/mcp/resources/sessions-all.ts +0 -19
  110. package/src/mcp/schemas/sessions.ts +0 -3
  111. package/src/mcp/server.ts +0 -17
  112. package/src/mcp/services/api-adapter.ts +0 -48
  113. package/src/mcp/services/data-source.ts +0 -22
  114. package/src/mcp/services/store-adapter.ts +0 -40
  115. package/src/mcp/tools/get-active-sessions.ts +0 -16
  116. package/src/mcp/tools/get-session-children.ts +0 -24
  117. package/src/mcp/tools/get-session-messages.ts +0 -24
  118. package/src/mcp/tools/get-session.ts +0 -23
  119. package/src/mcp/tools/index.ts +0 -15
  120. package/src/mcp/tools/list-sessions.ts +0 -16
  121. package/src/oc/index.ts +0 -302
  122. package/src/service/manager.ts +0 -284
  123. package/src/shared/config.ts +0 -511
  124. package/src/shared/ipc.ts +0 -80
  125. package/src/shared/logger.ts +0 -248
  126. package/src/shared/paths.ts +0 -73
  127. package/src/shared/platform.ts +0 -110
  128. package/src/shared/session-exclusions.ts +0 -5
  129. package/src/shared/session-view.ts +0 -302
  130. package/src/shared/terminal-capabilities.ts +0 -130
  131. package/src/shared/types.ts +0 -330
  132. package/src/shared/utils.ts +0 -339
  133. package/src/shared/version.ts +0 -44
  134. package/src/tui/App.tsx +0 -240
  135. package/src/tui/client.ts +0 -163
  136. package/src/tui/components/BindingsModal.tsx +0 -82
  137. package/src/tui/components/DashboardHeader.tsx +0 -256
  138. package/src/tui/components/KeyBar.tsx +0 -67
  139. package/src/tui/components/SessionDetailModal.tsx +0 -169
  140. package/src/tui/components/SessionsView.tsx +0 -184
  141. package/src/tui/components/TabBar.tsx +0 -40
  142. package/src/tui/components/UsageSummaryPane.tsx +0 -272
  143. package/src/tui/components/UsageView.tsx +0 -242
  144. package/src/tui/components/tool-icons.ts +0 -24
  145. package/src/tui/index.tsx +0 -106
  146. package/src/tui/keybindings.ts +0 -49
  147. package/src/tui/model.ts +0 -295
  148. package/src/tui/state.ts +0 -77
  149. package/src/tui/theme.ts +0 -110
  150. package/src/types.ts +0 -113
  151. package/src/usage/README.md +0 -323
  152. package/src/usage/api.ts +0 -99
  153. package/src/usage/client.ts +0 -3
  154. package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
  155. package/src/usage/clients/anthropic-web-usage.ts +0 -329
  156. package/src/usage/clients/claude-cli-probe.ts +0 -513
  157. package/src/usage/clients/codex-app-server.ts +0 -354
  158. package/src/usage/clients/codex-oauth-usage.ts +0 -240
  159. package/src/usage/clients/opencode-go-api.ts +0 -100
  160. package/src/usage/clients/opencode-go-usage.ts +0 -577
  161. package/src/usage/errors.ts +0 -33
  162. package/src/usage/index.ts +0 -20
  163. package/src/usage/local/opencode-go-local.ts +0 -194
  164. package/src/usage/mappers.ts +0 -282
  165. package/src/usage/planner.ts +0 -118
  166. package/src/usage/providers/claude.ts +0 -233
  167. package/src/usage/providers/codex.ts +0 -115
  168. package/src/usage/providers/opencode-go.ts +0 -115
  169. package/src/usage/resolver.ts +0 -4
  170. package/src/usage/resolvers/browser-cookies.ts +0 -808
  171. package/src/usage/resolvers/claude-auth.ts +0 -208
  172. package/src/usage/resolvers/claude-cli.ts +0 -24
  173. package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
  174. package/src/usage/resolvers/claude-web.ts +0 -82
  175. package/src/usage/resolvers/codex-auth.ts +0 -174
  176. package/src/usage/resolvers/keychain-coordinator.ts +0 -397
  177. package/src/usage/resolvers/opencode-go-web.ts +0 -55
  178. package/src/usage/types.ts +0 -225
  179. package/src/usage/web-session-cache.ts +0 -37
  180. /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
  181. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
  182. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
  183. /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
  184. /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
  185. /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
  186. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
  187. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
  188. /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
  189. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
  190. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
  191. /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
  192. /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
  193. /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
  194. /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
  195. /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
@@ -1,492 +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
- windowName: pane.windowName,
173
- pane: pane.paneId,
174
- },
175
- legacyPaneId: pane.paneId,
176
- processTree: result.pane.processTree,
177
- processChain: result.pane.processChain,
178
- sample: result.pane.sample,
179
- stableForMs: managedStableForMs(pluginSession),
180
- currentCommand: pane.currentCommand,
181
- currentPath: pane.currentPath,
182
- panePid: pane.panePid,
183
- tty: pane.tty,
184
- paneTitle: pane.title,
185
- };
186
- this.logger.debug("tmux", "Pane matched by plugin pane option — enriching", {
187
- paneId: pane.paneId,
188
- sessionId: pane.opencodeSession,
189
- });
190
- store.upsertSession(enriched);
191
- continue;
192
- }
193
- // Exact session ID is authoritative. If the referenced session is not in
194
- // the store yet, do not attempt any automatic OpenCode pane matching.
195
- this.logger.debug("tmux", "Plugin pane option set but session not found in store yet", {
196
- paneId: pane.paneId,
197
- opencodeSession: pane.opencodeSession,
198
- });
199
- newFallbackSessions.push(paneModelToSession(result.pane));
200
- continue;
201
- }
202
-
203
- // ── M1: hook-written @claude-session pane option ─────────────────────────
204
- // The scrylog-claude-hook binary writes @claude-session to the pane when
205
- // Claude runs inside tmux. This is an exact, authoritative match.
206
- if (pane.claudeSession) {
207
- const current = store.getSessions();
208
- const linkedSession = current.find(
209
- (s) =>
210
- isClaudeManagedSession(s) &&
211
- s.claude?.sessionId === pane.claudeSession &&
212
- (!s.legacyPaneId || s.legacyPaneId === pane.paneId),
213
- );
214
- if (linkedSession) {
215
- // Only apply detectClaudePermissionPrompt for non-hook-managed sessions.
216
- // Hook-managed sessions have authoritative state from the PermissionRequest
217
- // event and must not be overridden by stale tmux screen sampling.
218
- const claudePrompt = linkedSession.detectionSource === "claude-hook"
219
- ? null
220
- : detectClaudePermissionPrompt(result.pane.sample);
221
- store.clearTmuxBindingForPane(pane.paneId, linkedSession.id);
222
- const enriched: SessionModel = {
223
- ...linkedSession,
224
- source: "tmux",
225
- ...(claudePrompt ? {
226
- state: claudePrompt.state,
227
- hasAttention: claudePrompt.hasAttention,
228
- summary: claudePrompt.summary,
229
- } : {}),
230
- project: result.pane.project,
231
- tmux: {
232
- session: pane.sessionName,
233
- window: String(pane.windowIndex),
234
- windowName: pane.windowName,
235
- pane: pane.paneId,
236
- },
237
- legacyPaneId: pane.paneId,
238
- processTree: result.pane.processTree,
239
- processChain: result.pane.processChain,
240
- sample: result.pane.sample,
241
- stableForMs: managedStableForMs(linkedSession),
242
- currentCommand: pane.currentCommand,
243
- currentPath: pane.currentPath,
244
- panePid: pane.panePid,
245
- tty: pane.tty,
246
- paneTitle: pane.title,
247
- };
248
- this.logger.debug("tmux", "Pane matched by @claude-session option — enriching", {
249
- paneId: pane.paneId,
250
- claudeSessionId: pane.claudeSession,
251
- });
252
- store.upsertSession(enriched);
253
- continue;
254
- }
255
- // Check if deliberately excluded (local-command batch run)
256
- if (store.isLocalCommand(pane.claudeSession)) {
257
- this.logger.debug("tmux", "Skipping fallback for local-command session", {
258
- paneId: pane.paneId,
259
- claudeSession: pane.claudeSession,
260
- });
261
- continue;
262
- }
263
- // Authoritative but not yet in store — keep as fallback
264
- this.logger.debug("tmux", "@claude-session set but session not in store yet", {
265
- paneId: pane.paneId,
266
- claudeSession: pane.claudeSession,
267
- });
268
- newFallbackSessions.push(paneModelToSession(result.pane));
269
- continue;
270
- }
271
-
272
- // ── M1.5: hook-written @codex-session pane option ────────────────────────
273
- // The scrylog-codex-hook binary writes @codex-session to the pane when
274
- // Codex runs inside tmux. This is an exact, authoritative match.
275
- if (pane.codexSession) {
276
- const current = store.getSessions();
277
- const linkedSession = current.find(
278
- (s) =>
279
- isCodexManagedSession(s) &&
280
- s.codex?.sessionId === pane.codexSession &&
281
- (!s.legacyPaneId || s.legacyPaneId === pane.paneId),
282
- );
283
- if (linkedSession) {
284
- const codexPrompt = linkedSession.detectionSource === "codex-hook"
285
- ? null
286
- : detectCodexPermissionPrompt(result.pane.sample);
287
- store.clearTmuxBindingForPane(pane.paneId, linkedSession.id);
288
- const enriched: SessionModel = {
289
- ...linkedSession,
290
- source: "tmux",
291
- ...(codexPrompt ? {
292
- state: codexPrompt.state,
293
- hasAttention: codexPrompt.hasAttention,
294
- summary: codexPrompt.summary,
295
- } : {}),
296
- project: result.pane.project,
297
- tmux: {
298
- session: pane.sessionName,
299
- window: String(pane.windowIndex),
300
- windowName: pane.windowName,
301
- pane: pane.paneId,
302
- },
303
- legacyPaneId: pane.paneId,
304
- processTree: result.pane.processTree,
305
- processChain: result.pane.processChain,
306
- sample: result.pane.sample,
307
- stableForMs: managedStableForMs(linkedSession),
308
- currentCommand: pane.currentCommand,
309
- currentPath: pane.currentPath,
310
- panePid: pane.panePid,
311
- tty: pane.tty,
312
- paneTitle: pane.title,
313
- };
314
- this.logger.debug("tmux", "Pane matched by @codex-session option — enriching", {
315
- paneId: pane.paneId,
316
- codexSessionId: pane.codexSession,
317
- });
318
- store.upsertSession(enriched);
319
- continue;
320
- }
321
- // Authoritative but not yet in store — keep as fallback
322
- this.logger.debug("tmux", "@codex-session set but session not in store yet", {
323
- paneId: pane.paneId,
324
- codexSession: pane.codexSession,
325
- });
326
- newFallbackSessions.push(paneModelToSession(result.pane));
327
- continue;
328
- }
329
-
330
- // Claude panes without the pane option: keep as fallback to avoid duplicate
331
- // entries once the hook fires and the pane option is set.
332
- if (isClaudePaneCandidate(processTree, pane.currentCommand)) {
333
- newFallbackSessions.push(paneModelToSession(result.pane));
334
- continue;
335
- }
336
-
337
- // Codex panes without the pane option: keep as fallback.
338
- if (isCodexPaneCandidate(processTree, pane.currentCommand)) {
339
- newFallbackSessions.push(paneModelToSession(result.pane));
340
- continue;
341
- }
342
-
343
- // OpenCode panes inside tmux must only correlate by exact session ID.
344
- // Without the plugin-written pane option, keep a temporary fallback pane
345
- // row and avoid heuristic matching to DB/SDK sessions.
346
- if (isOpenCodePaneCandidate(pane, processTree, result.pane.tool)) {
347
- newFallbackSessions.push(paneModelToSession(result.pane));
348
- continue;
349
- }
350
-
351
- if (matchedSdkPid !== undefined) {
352
- // Find the SDK sessions for this instance and enrich them with tmux context.
353
- const instanceUrl = sdkPidMap.get(matchedSdkPid);
354
- const current = store.getSessions();
355
- const sdkSessionsForInstance = current.filter(
356
- (s) =>
357
- isOpenCodeManagedSession(s) &&
358
- s.opencode?.instanceUrl === instanceUrl &&
359
- !s.legacyPaneId,
360
- );
361
-
362
- for (const sdkSession of sdkSessionsForInstance) {
363
- const enriched: SessionModel = {
364
- ...sdkSession,
365
- // Switch to tmux source so TUI groups it with tmux sessions
366
- source: "tmux",
367
- project: result.pane.project,
368
- tmux: {
369
- session: pane.sessionName,
370
- window: String(pane.windowIndex),
371
- windowName: pane.windowName,
372
- pane: pane.paneId,
373
- },
374
- legacyPaneId: pane.paneId,
375
- // Extended fields from pane analysis
376
- processTree: result.pane.processTree,
377
- processChain: result.pane.processChain,
378
- sample: result.pane.sample,
379
- stableForMs: managedStableForMs(sdkSession),
380
- currentCommand: pane.currentCommand,
381
- currentPath: pane.currentPath,
382
- panePid: pane.panePid,
383
- tty: pane.tty,
384
- paneTitle: pane.title,
385
- };
386
- this.logger.debug("tmux", "Pane covered by SDK — enriching SDK session", {
387
- paneId: pane.paneId,
388
- sdkSessionId: sdkSession.id,
389
- instanceUrl,
390
- });
391
- store.upsertSession(enriched);
392
- }
393
- // Don't create a fallback entry for this pane
394
- this.logger.debug("tmux", "Pane covered by SDK — skipping fallback entry", {
395
- paneId: pane.paneId,
396
- matchedSdkPid,
397
- instanceUrl,
398
- });
399
- } else {
400
- // PID ancestry via sdkPidMap failed (e.g. registerSdkPid not yet called).
401
- // Attempt URL-from-args matching: walk the pane's ancestor chain looking
402
- // for an opencode process and derive the instance URL from its args.
403
- // This is deterministic and has no false positives unlike path matching.
404
- const ancestor = findOpencodeInTree(processTree);
405
- if (ancestor) {
406
- const current = store.getSessions();
407
- const sdkSessionsForInstance = current.filter(
408
- (s) =>
409
- isOpenCodeManagedSession(s) &&
410
- s.opencode?.instanceUrl === ancestor.url &&
411
- !s.legacyPaneId,
412
- );
413
-
414
- if (sdkSessionsForInstance.length > 0) {
415
- for (const sdkSession of sdkSessionsForInstance) {
416
- const enriched: SessionModel = {
417
- ...sdkSession,
418
- source: "tmux",
419
- project: result.pane.project,
420
- tmux: {
421
- session: pane.sessionName,
422
- window: String(pane.windowIndex),
423
- windowName: pane.windowName,
424
- pane: pane.paneId,
425
- },
426
- legacyPaneId: pane.paneId,
427
- processTree: result.pane.processTree,
428
- processChain: result.pane.processChain,
429
- sample: result.pane.sample,
430
- stableForMs: managedStableForMs(sdkSession),
431
- currentCommand: pane.currentCommand,
432
- currentPath: pane.currentPath,
433
- panePid: pane.panePid,
434
- tty: pane.tty,
435
- paneTitle: pane.title,
436
- };
437
- this.logger.debug("tmux", "Pane matched SDK session by URL-from-args — enriching", {
438
- paneId: pane.paneId,
439
- sdkSessionId: sdkSession.id,
440
- instanceUrl: ancestor.url,
441
- });
442
- store.upsertSession(enriched);
443
- }
444
- this.logger.debug("tmux", "Pane covered by URL-from-args match — skipping fallback", {
445
- paneId: pane.paneId,
446
- instanceUrl: ancestor.url,
447
- });
448
- } else {
449
- newFallbackSessions.push(paneModelToSession(result.pane));
450
- }
451
- } else {
452
- newFallbackSessions.push(paneModelToSession(result.pane));
453
- }
454
- }
455
- }
456
-
457
- // Prune history for gone panes
458
- const livePaneIds = new Set(tmux.panes.map((pane) => pane.paneId));
459
- clearStaleOpenCodeTmuxBindings(store, livePaneIds);
460
- clearStaleClaudeTmuxBindings(store, livePaneIds);
461
- clearStaleCodexTmuxBindings(store, livePaneIds);
462
-
463
- for (const paneId of [...this.history.keys()]) {
464
- if (!livePaneIds.has(paneId)) {
465
- this.history.delete(paneId);
466
- }
467
- }
468
-
469
- // Replace only fallback sessions (non-enriched); keep SDK-sourced sessions
470
- const current = store.getSessions();
471
- const nonFallback = current.filter(
472
- (s) => !(s.source === "tmux" && s.detectionSource === "fallback-detection"),
473
- );
474
-
475
- // Fallback sessions are rebuilt from scratch every poll. Git project
476
- // resolution can transiently fail (or currentPath can come through empty),
477
- // so inherit the previously-resolved project/currentPath to keep the TUI
478
- // group membership stable instead of oscillating between polls.
479
- const previousFallback = new Map(
480
- current
481
- .filter((s) => s.source === "tmux" && s.detectionSource === "fallback-detection")
482
- .map((s) => [s.id, s]),
483
- );
484
- const mergedFallbacks = newFallbackSessions.map((session) => {
485
- const prev = previousFallback.get(session.id);
486
- if (!prev || session.project) return session;
487
- return { ...session, project: prev.project, currentPath: session.currentPath || prev.currentPath };
488
- });
489
-
490
- store.setSessions([...nonFallback, ...mergedFallbacks]);
491
- }
492
- }