mstro-app 0.4.51 → 0.5.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 (223) hide show
  1. package/README.md +10 -5
  2. package/bin/mstro.js +1 -1
  3. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  4. package/dist/server/cli/headless/claude-invoker-stall.js +7 -2
  5. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +1 -1
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +63 -67
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  12. package/dist/server/cli/headless/stall-assessor.js +9 -4
  13. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  14. package/dist/server/cli/improvisation-history-store.d.ts +16 -0
  15. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
  16. package/dist/server/cli/improvisation-history-store.js +52 -0
  17. package/dist/server/cli/improvisation-history-store.js.map +1 -0
  18. package/dist/server/cli/improvisation-movements.d.ts +31 -0
  19. package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
  20. package/dist/server/cli/improvisation-movements.js +93 -0
  21. package/dist/server/cli/improvisation-movements.js.map +1 -0
  22. package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
  23. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
  24. package/dist/server/cli/improvisation-output-queue.js +40 -0
  25. package/dist/server/cli/improvisation-output-queue.js.map +1 -0
  26. package/dist/server/cli/improvisation-retry.d.ts +21 -51
  27. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  28. package/dist/server/cli/improvisation-retry.js +18 -433
  29. package/dist/server/cli/improvisation-retry.js.map +1 -1
  30. package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
  31. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  32. package/dist/server/cli/improvisation-session-manager.js +53 -148
  33. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  34. package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
  35. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
  36. package/dist/server/cli/retry/retry-best-result.js +61 -0
  37. package/dist/server/cli/retry/retry-best-result.js.map +1 -0
  38. package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
  39. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
  40. package/dist/server/cli/retry/retry-context-loss.js +68 -0
  41. package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
  42. package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
  43. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
  44. package/dist/server/cli/retry/retry-premature-completion.js +81 -0
  45. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
  46. package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
  47. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
  48. package/dist/server/cli/retry/retry-recovery-strategies.js +166 -0
  49. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
  50. package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
  51. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
  52. package/dist/server/cli/retry/retry-resume-strategy.js +22 -0
  53. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
  54. package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
  55. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
  56. package/dist/server/cli/retry/retry-runner-factory.js +60 -0
  57. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
  58. package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
  59. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
  60. package/dist/server/cli/retry/retry-tool-results.js +24 -0
  61. package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
  62. package/dist/server/cli/retry/retry-types.d.ts +30 -0
  63. package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
  64. package/dist/server/cli/retry/retry-types.js +4 -0
  65. package/dist/server/cli/retry/retry-types.js.map +1 -0
  66. package/dist/server/index.js +21 -109
  67. package/dist/server/index.js.map +1 -1
  68. package/dist/server/server-setup.d.ts +16 -1
  69. package/dist/server/server-setup.d.ts.map +1 -1
  70. package/dist/server/server-setup.js +107 -0
  71. package/dist/server/server-setup.js.map +1 -1
  72. package/dist/server/services/plan/board-config.d.ts +21 -0
  73. package/dist/server/services/plan/board-config.d.ts.map +1 -0
  74. package/dist/server/services/plan/board-config.js +112 -0
  75. package/dist/server/services/plan/board-config.js.map +1 -0
  76. package/dist/server/services/plan/composer.d.ts +1 -1
  77. package/dist/server/services/plan/composer.d.ts.map +1 -1
  78. package/dist/server/services/plan/composer.js +7 -5
  79. package/dist/server/services/plan/composer.js.map +1 -1
  80. package/dist/server/services/plan/executor.d.ts +48 -48
  81. package/dist/server/services/plan/executor.d.ts.map +1 -1
  82. package/dist/server/services/plan/executor.js +157 -455
  83. package/dist/server/services/plan/executor.js.map +1 -1
  84. package/dist/server/services/plan/issue-loader.d.ts +16 -0
  85. package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
  86. package/dist/server/services/plan/issue-loader.js +46 -0
  87. package/dist/server/services/plan/issue-loader.js.map +1 -0
  88. package/dist/server/services/plan/issue-writer.d.ts +34 -0
  89. package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
  90. package/dist/server/services/plan/issue-writer.js +110 -0
  91. package/dist/server/services/plan/issue-writer.js.map +1 -0
  92. package/dist/server/services/plan/output-manager.d.ts.map +1 -1
  93. package/dist/server/services/plan/output-manager.js +2 -1
  94. package/dist/server/services/plan/output-manager.js.map +1 -1
  95. package/dist/server/services/plan/progress-log.d.ts +11 -0
  96. package/dist/server/services/plan/progress-log.d.ts.map +1 -0
  97. package/dist/server/services/plan/progress-log.js +81 -0
  98. package/dist/server/services/plan/progress-log.js.map +1 -0
  99. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  100. package/dist/server/services/plan/prompt-builder.js +48 -31
  101. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  102. package/dist/server/services/plan/readiness-planner.d.ts +15 -0
  103. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
  104. package/dist/server/services/plan/readiness-planner.js +41 -0
  105. package/dist/server/services/plan/readiness-planner.js.map +1 -0
  106. package/dist/server/services/plan/review-gate.d.ts +31 -0
  107. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  108. package/dist/server/services/plan/review-gate.js +52 -2
  109. package/dist/server/services/plan/review-gate.js.map +1 -1
  110. package/dist/server/services/platform.d.ts +56 -0
  111. package/dist/server/services/platform.d.ts.map +1 -1
  112. package/dist/server/services/platform.js +154 -52
  113. package/dist/server/services/platform.js.map +1 -1
  114. package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
  115. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
  116. package/dist/server/services/websocket/file-download-handler.js +165 -0
  117. package/dist/server/services/websocket/file-download-handler.js.map +1 -0
  118. package/dist/server/services/websocket/git-branch-handlers.d.ts +1 -1
  119. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  120. package/dist/server/services/websocket/git-branch-handlers.js +21 -1
  121. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  122. package/dist/server/services/websocket/git-handlers.js +1 -1
  123. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  124. package/dist/server/services/websocket/git-worktree-handlers.d.ts +2 -0
  125. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  126. package/dist/server/services/websocket/git-worktree-handlers.js +30 -4
  127. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  128. package/dist/server/services/websocket/handler-context.d.ts +15 -0
  129. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  130. package/dist/server/services/websocket/handler.d.ts +7 -0
  131. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  132. package/dist/server/services/websocket/handler.js +73 -11
  133. package/dist/server/services/websocket/handler.js.map +1 -1
  134. package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
  135. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
  136. package/dist/server/services/websocket/msg-id-tracker.js +77 -0
  137. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
  138. package/dist/server/services/websocket/quality-handlers.js +15 -3
  139. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  140. package/dist/server/services/websocket/quality-review-agent.js +2 -2
  141. package/dist/server/services/websocket/session-handlers.d.ts +48 -2
  142. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  143. package/dist/server/services/websocket/session-handlers.js +204 -65
  144. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  145. package/dist/server/services/websocket/session-initialization.d.ts +2 -2
  146. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  147. package/dist/server/services/websocket/session-initialization.js +75 -17
  148. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  149. package/dist/server/services/websocket/session-registry.d.ts +29 -1
  150. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  151. package/dist/server/services/websocket/session-registry.js +53 -4
  152. package/dist/server/services/websocket/session-registry.js.map +1 -1
  153. package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
  154. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
  155. package/dist/server/services/websocket/tab-broadcast.js +13 -0
  156. package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
  157. package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
  158. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
  159. package/dist/server/services/websocket/tab-event-buffer.js +107 -0
  160. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
  161. package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
  162. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
  163. package/dist/server/services/websocket/tab-event-replay.js +21 -0
  164. package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
  165. package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
  166. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  167. package/dist/server/services/websocket/tab-handlers.js +2 -9
  168. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  169. package/dist/server/services/websocket/types.d.ts +15 -6
  170. package/dist/server/services/websocket/types.d.ts.map +1 -1
  171. package/dist/server/services/websocket/types.js +6 -4
  172. package/dist/server/services/websocket/types.js.map +1 -1
  173. package/package.json +1 -1
  174. package/server/README.md +1 -1
  175. package/server/cli/headless/claude-invoker-stall.ts +7 -2
  176. package/server/cli/headless/claude-invoker.ts +1 -1
  177. package/server/cli/headless/runner.ts +67 -72
  178. package/server/cli/headless/stall-assessor.ts +9 -4
  179. package/server/cli/headless/types.ts +1 -1
  180. package/server/cli/improvisation-history-store.ts +62 -0
  181. package/server/cli/improvisation-movements.ts +120 -0
  182. package/server/cli/improvisation-output-queue.ts +42 -0
  183. package/server/cli/improvisation-retry.ts +25 -600
  184. package/server/cli/improvisation-session-manager.ts +74 -160
  185. package/server/cli/retry/retry-best-result.ts +70 -0
  186. package/server/cli/retry/retry-context-loss.ts +87 -0
  187. package/server/cli/retry/retry-premature-completion.ts +113 -0
  188. package/server/cli/retry/retry-recovery-strategies.ts +247 -0
  189. package/server/cli/retry/retry-resume-strategy.ts +33 -0
  190. package/server/cli/retry/retry-runner-factory.ts +70 -0
  191. package/server/cli/retry/retry-tool-results.ts +31 -0
  192. package/server/cli/retry/retry-types.ts +32 -0
  193. package/server/index.ts +37 -123
  194. package/server/server-setup.ts +126 -1
  195. package/server/services/plan/agents/assess-stall.md +11 -4
  196. package/server/services/plan/board-config.ts +122 -0
  197. package/server/services/plan/composer.ts +7 -5
  198. package/server/services/plan/executor.ts +214 -467
  199. package/server/services/plan/issue-loader.ts +64 -0
  200. package/server/services/plan/issue-writer.ts +137 -0
  201. package/server/services/plan/output-manager.ts +2 -1
  202. package/server/services/plan/progress-log.ts +92 -0
  203. package/server/services/plan/prompt-builder.ts +73 -35
  204. package/server/services/plan/readiness-planner.ts +50 -0
  205. package/server/services/plan/review-gate.ts +102 -2
  206. package/server/services/platform.ts +163 -58
  207. package/server/services/websocket/file-download-handler.ts +191 -0
  208. package/server/services/websocket/git-branch-handlers.ts +28 -1
  209. package/server/services/websocket/git-handlers.ts +1 -1
  210. package/server/services/websocket/git-worktree-handlers.ts +31 -4
  211. package/server/services/websocket/handler-context.ts +15 -0
  212. package/server/services/websocket/handler.ts +76 -12
  213. package/server/services/websocket/msg-id-tracker.ts +84 -0
  214. package/server/services/websocket/quality-handlers.ts +16 -3
  215. package/server/services/websocket/quality-review-agent.ts +2 -2
  216. package/server/services/websocket/session-handlers.ts +213 -68
  217. package/server/services/websocket/session-initialization.ts +83 -19
  218. package/server/services/websocket/session-registry.ts +61 -4
  219. package/server/services/websocket/tab-broadcast.ts +38 -0
  220. package/server/services/websocket/tab-event-buffer.ts +159 -0
  221. package/server/services/websocket/tab-event-replay.ts +42 -0
  222. package/server/services/websocket/tab-handlers.ts +2 -9
  223. package/server/services/websocket/types.ts +17 -4
@@ -1,11 +1,13 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
- import type { FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
4
+ import { type FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
5
5
  import { getEffortLevel, getModel } from '../settings.js';
6
6
  import type { HandlerContext } from './handler-context.js';
7
7
  import { runQualityScan } from './quality-service.js';
8
+ import type { SessionRegistry } from './session-registry.js';
8
9
  import { resolveSkillPrompt } from './skill-handlers.js';
10
+ import { broadcastTabEvent } from './tab-broadcast.js';
9
11
  import type { WebSocketMessage, WSContext } from './types.js';
10
12
 
11
13
  // Re-export from extracted modules for backward compatibility
@@ -43,6 +45,17 @@ function convertToolHistoryToLines(tools: Array<{ toolName: string; toolInput?:
43
45
  return lines;
44
46
  }
45
47
 
48
+ function formatElapsedDuration(totalSeconds: number): string {
49
+ const seconds = Math.floor(totalSeconds) % 60;
50
+ const minutes = Math.floor(totalSeconds / 60) % 60;
51
+ const hours = Math.floor(totalSeconds / 3600) % 24;
52
+ const days = Math.floor(totalSeconds / 86400);
53
+ if (days > 0) return `${days}d ${hours}h ${minutes}m ${seconds}s`;
54
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
55
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
56
+ return `${seconds}s`;
57
+ }
58
+
46
59
  /** Convert a single movement record into OutputLine-compatible entries */
47
60
  function convertMovementToLines(movement: { userPrompt: string; timestamp: string; thinkingOutput?: string; toolUseHistory?: Array<{ toolName: string; toolInput?: Record<string, unknown>; result?: string; isError?: boolean }>; assistantResponse?: string; errorOutput?: string; tokensUsed: number; durationMs?: number }): Array<Record<string, unknown>> {
48
61
  const lines: Array<Record<string, unknown>> = [];
@@ -67,26 +80,105 @@ function convertMovementToLines(movement: { userPrompt: string; timestamp: strin
67
80
  }
68
81
 
69
82
  const durationText = movement.durationMs
70
- ? `Completed in ${(movement.durationMs / 1000).toFixed(2)}s`
83
+ ? `Completed in ${formatElapsedDuration(movement.durationMs / 1000)}`
71
84
  : 'Completed';
72
85
  lines.push({ type: 'system', text: durationText, timestamp: ts });
73
86
  return lines;
74
87
  }
75
88
 
76
89
  function requireSession(ctx: HandlerContext, ws: WSContext, tabId: string): ImprovisationSessionManager {
77
- const session = getSession(ctx, ws, tabId);
90
+ const session = resolveTabSession(ctx, ws, tabId);
78
91
  if (!session) throw new Error(`No session found for tab ${tabId}`);
79
92
  return session;
80
93
  }
81
94
 
82
- function getSession(ctx: HandlerContext, ws: WSContext, tabId: string): ImprovisationSessionManager | null {
95
+ /**
96
+ * Canonical tab → session resolver.
97
+ *
98
+ * Returns the `ImprovisationSessionManager` for `tabId`, attaching it to `ws`
99
+ * if needed. Contract: after a successful return, the session is mapped in
100
+ * `ctx.connections.get(ws)` and its event listeners are wired to this `ws`.
101
+ *
102
+ * Resolution order (cheapest first, each step caches for subsequent calls):
103
+ * 1. Per-connection `tabMap` — the session is already attached to this `ws`.
104
+ * 2. Registry + in-memory — another connection has the session loaded;
105
+ * re-attach listeners to this `ws` without re-reading disk.
106
+ * 3. Registry + disk — session is persisted but not in memory (e.g. after
107
+ * a CLI restart); construct the manager from history and cache it.
108
+ *
109
+ * Returns `null` only when the tab is truly unknown (no registry entry AND
110
+ * no history file). That is the only case where handlers should surface an
111
+ * error to the caller — everything else self-heals so session ops never
112
+ * race the `initTab` handshake.
113
+ *
114
+ * Also restores worktree bindings from the registry on miss so git/file ops
115
+ * against this tab route to the correct directory even without initTab.
116
+ */
117
+ export function resolveTabSession(ctx: HandlerContext, ws: WSContext, tabId: string): ImprovisationSessionManager | null {
83
118
  const tabMap = ctx.connections.get(ws);
84
- if (!tabMap) return null;
85
119
 
86
- const sessionId = tabMap.get(tabId);
87
- if (!sessionId) return null;
120
+ const mappedSessionId = tabMap?.get(tabId);
121
+ if (mappedSessionId) {
122
+ const session = ctx.sessions.get(mappedSessionId);
123
+ if (session) return session;
124
+ }
125
+
126
+ const workingDir = ws._workingDir;
127
+ if (!workingDir) return null;
128
+
129
+ const registry = ctx.getRegistry(workingDir);
130
+ const registrySessionId = registry.getTabSession(tabId);
131
+ if (!registrySessionId) return null;
132
+
133
+ const inMemorySession = ctx.sessions.get(registrySessionId);
134
+ if (inMemorySession) {
135
+ return attachSessionToConnection(ctx, ws, tabId, inMemorySession, registry);
136
+ }
137
+
138
+ try {
139
+ const diskSession = ImprovisationSessionManager.resumeFromHistory(workingDir, registrySessionId);
140
+ ctx.sessions.set(diskSession.getSessionInfo().sessionId, diskSession);
141
+ registry.markTabPersisted(tabId);
142
+ return attachSessionToConnection(ctx, ws, tabId, diskSession, registry);
143
+ } catch {
144
+ // History file doesn't exist — either the tab has never had a first
145
+ // prompt (lazy persistence) or the file was deleted and the registry
146
+ // hasn't been swept yet. Either way, construct a fresh session bound to
147
+ // the registered sessionId so the tab keeps its identity.
148
+ const freshSession = new ImprovisationSessionManager({
149
+ workingDir,
150
+ sessionId: registrySessionId,
151
+ model: getModel(),
152
+ effortLevel: getEffortLevel(),
153
+ });
154
+ ctx.sessions.set(freshSession.getSessionInfo().sessionId, freshSession);
155
+ return attachSessionToConnection(ctx, ws, tabId, freshSession, registry);
156
+ }
157
+ }
158
+
159
+ /** Wire listeners + update caches when a resolved session first attaches to this ws. */
160
+ function attachSessionToConnection(
161
+ ctx: HandlerContext,
162
+ ws: WSContext,
163
+ tabId: string,
164
+ session: ImprovisationSessionManager,
165
+ registry: SessionRegistry,
166
+ ): ImprovisationSessionManager {
167
+ setupSessionListeners(ctx, session, ws, tabId);
168
+ const tabMap = ctx.connections.get(ws);
169
+ if (tabMap) tabMap.set(tabId, session.getSessionInfo().sessionId);
170
+ registry.touchTab(tabId);
171
+ restoreWorktreeFromRegistry(ctx, registry, tabId);
172
+ return session;
173
+ }
88
174
 
89
- return ctx.sessions.get(sessionId) || null;
175
+ /** Copy worktree bindings from the persistent registry into the live context. */
176
+ export function restoreWorktreeFromRegistry(ctx: HandlerContext, registry: SessionRegistry, tabId: string): void {
177
+ if (ctx.gitDirectories.has(tabId)) return;
178
+ const regTab = registry.getTab(tabId);
179
+ if (!regTab?.worktreePath) return;
180
+ ctx.gitDirectories.set(tabId, regTab.worktreePath);
181
+ if (regTab.worktreeBranch) ctx.gitBranches.set(tabId, regTab.worktreeBranch);
90
182
  }
91
183
 
92
184
  export function buildOutputHistory(session: ImprovisationSessionManager): Array<Record<string, unknown>> {
@@ -100,24 +192,49 @@ export function buildOutputHistory(session: ImprovisationSessionManager): Array<
100
192
  .flatMap(convertMovementToLines);
101
193
  }
102
194
 
103
- export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, ws: WSContext, tabId: string): void {
195
+ /**
196
+ * Wire session events to the WebSocket fan-out.
197
+ *
198
+ * All session-driven messages broadcast to `allConnections` rather than the
199
+ * `ws` that called `initTab`/`execute`. The CLI has exactly one live socket
200
+ * at a time (the platform relay), and `allConnections` is maintained by
201
+ * `handleConnection`/`handleClose` — so a broadcast always lands on the
202
+ * *current* relay socket, even after a reconnect, and is fanned out to every
203
+ * paired web client by the platform.
204
+ *
205
+ * Sending to the captured `ws` was the prior shape and silently dropped
206
+ * streaming output for any tab whose `setupSessionListeners` hadn't been
207
+ * re-run after a relay reconnect (i.e. background tabs the user wasn't
208
+ * actively viewing). The `tabStateChanged` events still fired — they were
209
+ * already broadcast — so the tab's executing dot showed up but the actual
210
+ * stream content (`output`/`thinking`/`toolUse`/...) went nowhere.
211
+ *
212
+ * `ws` is retained in the signature for symmetry with other handlers and to
213
+ * keep the call sites unchanged.
214
+ */
215
+ export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, _ws: WSContext, tabId: string): void {
104
216
  session.removeAllListeners();
105
217
 
218
+ session.on('onHistoryPersisted', () => {
219
+ const registry = ctx.getRegistry('');
220
+ try { registry.markTabPersisted(tabId); } catch { /* ignore */ }
221
+ });
222
+
106
223
  session.on('onOutput', (text: string) => {
107
- ctx.send(ws, { type: 'output', tabId, data: { text, timestamp: Date.now() } });
224
+ broadcastTabEvent(ctx, tabId, 'output', { text, timestamp: Date.now() });
108
225
  });
109
226
 
110
227
  session.on('onThinking', (text: string) => {
111
- ctx.send(ws, { type: 'thinking', tabId, data: { text } });
228
+ broadcastTabEvent(ctx, tabId, 'thinking', { text });
112
229
  });
113
230
 
114
231
  session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean) => {
115
- ctx.send(ws, { type: 'movementStart', tabId, data: { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue } });
232
+ broadcastTabEvent(ctx, tabId, 'movementStart', { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue });
116
233
  ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
117
234
  });
118
235
 
119
236
  session.on('onMovementComplete', (movement: Record<string, unknown>) => {
120
- ctx.send(ws, { type: 'movementComplete', tabId, data: movement });
237
+ broadcastTabEvent(ctx, tabId, 'movementComplete', movement);
121
238
 
122
239
  const registry = ctx.getRegistry('');
123
240
  // Use a try/catch since getRegistry may not have been initialized with the right workingDir
@@ -141,24 +258,24 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
141
258
  });
142
259
 
143
260
  session.on('onMovementError', (error: Error) => {
144
- ctx.send(ws, { type: 'movementError', tabId, data: { message: error.message } });
261
+ broadcastTabEvent(ctx, tabId, 'movementError', { message: error.message });
145
262
  ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: false } });
146
263
  });
147
264
 
148
265
  session.on('onSessionUpdate', (history: Record<string, unknown>) => {
149
- ctx.send(ws, { type: 'sessionUpdate', tabId, data: history });
266
+ broadcastTabEvent(ctx, tabId, 'sessionUpdate', history);
150
267
  });
151
268
 
152
269
  session.on('onPlanNeedsConfirmation', (plan: Record<string, unknown>) => {
153
- ctx.send(ws, { type: 'approvalRequired', tabId, data: plan });
270
+ broadcastTabEvent(ctx, tabId, 'approvalRequired', plan);
154
271
  });
155
272
 
156
273
  session.on('onToolUse', (event: Record<string, unknown>) => {
157
- ctx.send(ws, { type: 'toolUse', tabId, data: { ...event, timestamp: Date.now() } });
274
+ broadcastTabEvent(ctx, tabId, 'toolUse', { ...event, timestamp: Date.now() });
158
275
  });
159
276
 
160
277
  session.on('onTokenUsage', (usage: { inputTokens: number; outputTokens: number }) => {
161
- ctx.send(ws, { type: 'streamingTokens', tabId, data: usage });
278
+ broadcastTabEvent(ctx, tabId, 'streamingTokens', usage);
162
279
  });
163
280
  }
164
281
 
@@ -187,46 +304,87 @@ export function mergePreUploadedAttachments(ctx: HandlerContext, tabId: string,
187
304
 
188
305
  const WRITE_OPS = new Set(['execute', 'cancel', 'new', 'approve', 'reject']);
189
306
 
307
+ function emitExecuteAck(ctx: HandlerContext, tabId: string, sessionId: string, msgId: string, duplicate = false): void {
308
+ ctx.broadcastToAll({
309
+ type: 'executeAck',
310
+ tabId,
311
+ data: duplicate ? { msgId, sessionId, duplicate: true } : { msgId, sessionId },
312
+ });
313
+ }
314
+
315
+ /**
316
+ * Handle an `execute` request: validate, dedupe on msgId, run the prompt,
317
+ * and ack every paired web so their outbox drains.
318
+ *
319
+ * Extracted from `handleSessionMessage` to keep its cyclomatic complexity
320
+ * within the biome threshold; the switch-body pattern was pushing past 15.
321
+ */
322
+ function handleExecuteMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string): void {
323
+ if (!msg.data?.prompt) throw new Error('Prompt is required');
324
+ const session = requireSession(ctx, ws, tabId);
325
+ const { sessionId } = session.getSessionInfo();
326
+ const msgId = typeof msg.data.msgId === 'string' ? msg.data.msgId as string : undefined;
327
+
328
+ // Idempotency: a web reconnect may replay the same msgId. Re-ack so its
329
+ // outbox drains, but don't run the prompt a second time.
330
+ if (msgId && !ctx.msgIdTracker.recordIfFirst(tabId, msgId)) {
331
+ console.log(`[session] execute duplicate msgId=${msgId} tabId=${tabId} — re-acking without re-run`);
332
+ emitExecuteAck(ctx, tabId, sessionId, msgId, /* duplicate */ true);
333
+ return;
334
+ }
335
+
336
+ if (msgId) {
337
+ console.log(`[session] execute accepted msgId=${msgId} tabId=${tabId} sessionId=${sessionId}`);
338
+ }
339
+
340
+ const worktreeDir = ctx.gitDirectories.get(tabId);
341
+ const attachments = mergePreUploadedAttachments(ctx, tabId, msg.data.attachments);
342
+
343
+ // Resolve slash commands (e.g. "/code-review") to their SKILL.md content.
344
+ // Claude Code's -p headless mode doesn't support skills natively, so we
345
+ // load the skill's instructions and pass them as the actual prompt.
346
+ const rawPrompt = msg.data.prompt as string;
347
+ const effectiveDir = worktreeDir || session.getSessionInfo().workingDir;
348
+ const resolved = resolveSkillPrompt(rawPrompt, effectiveDir);
349
+
350
+ session.executePrompt(
351
+ resolved ? resolved.prompt : rawPrompt,
352
+ attachments,
353
+ {
354
+ workingDir: worktreeDir,
355
+ displayPrompt: resolved ? rawPrompt : undefined,
356
+ },
357
+ );
358
+
359
+ // Ack AFTER enqueue so the web knows the CLI accepted the work.
360
+ if (msgId) emitExecuteAck(ctx, tabId, sessionId, msgId);
361
+ }
362
+
363
+ function handleNewSessionMessage(ctx: HandlerContext, ws: WSContext, tabId: string): void {
364
+ const oldSession = requireSession(ctx, ws, tabId);
365
+ const oldSessionId = oldSession.getSessionInfo().sessionId;
366
+ const newSession = oldSession.startNewSession({ model: getModel(), effortLevel: getEffortLevel() });
367
+ oldSession.destroy();
368
+ ctx.sessions.delete(oldSessionId);
369
+ setupSessionListeners(ctx, newSession, ws, tabId);
370
+ const newSessionId = newSession.getSessionInfo().sessionId;
371
+ ctx.sessions.set(newSessionId, newSession);
372
+ const tabMap = ctx.connections.get(ws);
373
+ if (tabMap) tabMap.set(tabId, newSessionId);
374
+ const registry = ctx.getRegistry('');
375
+ try { registry.updateTabSession(tabId, newSessionId); } catch { /* ignore */ }
376
+ ctx.send(ws, { type: 'newSession', tabId, data: newSession.getSessionInfo() });
377
+ }
378
+
190
379
  export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, permission?: 'view'): void {
191
380
  if (permission === 'view' && WRITE_OPS.has(msg.type)) {
192
381
  throw new Error('View-only users cannot perform write operations');
193
382
  }
194
383
 
195
384
  switch (msg.type) {
196
- case 'execute': {
197
- if (!msg.data?.prompt) throw new Error('Prompt is required');
198
- const session = requireSession(ctx, ws, tabId);
199
- const worktreeDir = ctx.gitDirectories.get(tabId);
200
- const attachments = mergePreUploadedAttachments(ctx, tabId, msg.data.attachments);
201
-
202
- // Resolve slash commands (e.g. "/code-review") to their SKILL.md prompt content.
203
- // Claude Code's -p headless mode doesn't support skills natively, so we load
204
- // the skill's instructions and pass them as the actual prompt.
205
- const rawPrompt = msg.data.prompt as string;
206
- const effectiveDir = worktreeDir || session.getSessionInfo().workingDir;
207
- const resolved = resolveSkillPrompt(rawPrompt, effectiveDir);
208
-
209
- // Authoritative prompt-input clear for all connected devices. The
210
- // submitter already cleared locally; this guarantees other devices
211
- // clear even if the submitter's debounced syncPromptText never fires
212
- // (e.g. mobile tab suspended after Send). Clients suppress this via
213
- // locallyEditingTabs if the user is actively typing a new prompt.
214
- ctx.broadcastToAll({
215
- type: 'promptTextSync',
216
- tabId,
217
- data: { tabId, text: '' },
218
- });
219
-
220
- session.executePrompt(
221
- resolved ? resolved.prompt : rawPrompt,
222
- attachments,
223
- {
224
- workingDir: worktreeDir,
225
- displayPrompt: resolved ? rawPrompt : undefined,
226
- },
227
- );
385
+ case 'execute':
386
+ handleExecuteMessage(ctx, ws, msg, tabId);
228
387
  break;
229
- }
230
388
  case 'cancel': {
231
389
  const session = requireSession(ctx, ws, tabId);
232
390
  session.cancel();
@@ -237,32 +395,19 @@ export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: We
237
395
  ctx.send(ws, { type: 'history', tabId, data: session.getHistory() });
238
396
  break;
239
397
  }
240
- case 'new': {
241
- const oldSession = requireSession(ctx, ws, tabId);
242
- const oldSessionId = oldSession.getSessionInfo().sessionId;
243
- const newSession = oldSession.startNewSession({ model: getModel(), effortLevel: getEffortLevel() });
244
- oldSession.destroy();
245
- ctx.sessions.delete(oldSessionId);
246
- setupSessionListeners(ctx, newSession, ws, tabId);
247
- const newSessionId = newSession.getSessionInfo().sessionId;
248
- ctx.sessions.set(newSessionId, newSession);
249
- const tabMap = ctx.connections.get(ws);
250
- if (tabMap) tabMap.set(tabId, newSessionId);
251
- const registry = ctx.getRegistry('');
252
- try { registry.updateTabSession(tabId, newSessionId); } catch { /* ignore */ }
253
- ctx.send(ws, { type: 'newSession', tabId, data: newSession.getSessionInfo() });
398
+ case 'new':
399
+ handleNewSessionMessage(ctx, ws, tabId);
254
400
  break;
255
- }
256
401
  case 'approve': {
257
402
  const session = requireSession(ctx, ws, tabId);
258
403
  session.respondToApproval(true);
259
- ctx.send(ws, { type: 'output', tabId, data: { text: '\n✅ Approved - proceeding with operation\n' } });
404
+ broadcastTabEvent(ctx, tabId, 'output', { text: '\n✅ Approved - proceeding with operation\n' });
260
405
  break;
261
406
  }
262
407
  case 'reject': {
263
408
  const session = requireSession(ctx, ws, tabId);
264
409
  session.respondToApproval(false);
265
- ctx.send(ws, { type: 'output', tabId, data: { text: '\n🚫 Rejected - operation cancelled\n' } });
410
+ broadcastTabEvent(ctx, tabId, 'output', { text: '\n🚫 Rejected - operation cancelled\n' });
266
411
  break;
267
412
  }
268
413
  }
@@ -6,8 +6,23 @@ import { getEffortLevel, getModel } from '../settings.js';
6
6
  import type { HandlerContext } from './handler-context.js';
7
7
  import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
8
8
  import type { SessionRegistry } from './session-registry.js';
9
+ import { replayTabEventsSince } from './tab-event-replay.js';
9
10
  import type { WSContext } from './types.js';
10
11
 
12
+ /**
13
+ * Extract `lastSeenSeq` from an initTab/resumeSession data payload.
14
+ *
15
+ * Keeps the narrow-typing scoped to the initialization module instead of
16
+ * leaking into the broader `HandlerContext`. Returns `undefined` for first
17
+ * init (no replay needed) or malformed payloads (treated as first init —
18
+ * safer than surfacing an error the user can't act on).
19
+ */
20
+ function extractLastSeenSeq(data: unknown): number | undefined {
21
+ if (!data || typeof data !== 'object') return undefined;
22
+ const candidate = (data as { lastSeenSeq?: unknown }).lastSeenSeq;
23
+ return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
24
+ }
25
+
11
26
  function tryResumeFromDisk(
12
27
  ctx: HandlerContext,
13
28
  ws: WSContext,
@@ -15,7 +30,8 @@ function tryResumeFromDisk(
15
30
  workingDir: string,
16
31
  registrySessionId: string,
17
32
  tabMap: Map<string, string> | undefined,
18
- registry: SessionRegistry
33
+ registry: SessionRegistry,
34
+ lastSeenSeq: number | undefined,
19
35
  ): boolean {
20
36
  try {
21
37
  const diskSession = ImprovisationSessionManager.resumeFromHistory(workingDir, registrySessionId);
@@ -24,6 +40,7 @@ function tryResumeFromDisk(
24
40
  ctx.sessions.set(diskSessionId, diskSession);
25
41
  if (tabMap) tabMap.set(tabId, diskSessionId);
26
42
  registry.touchTab(tabId);
43
+ registry.markTabPersisted(tabId);
27
44
 
28
45
  // Restore worktree state from registry
29
46
  const regTab = registry.getTab(tabId);
@@ -34,12 +51,18 @@ function tryResumeFromDisk(
34
51
  const worktreePath = ctx.gitDirectories.get(tabId);
35
52
  const worktreeBranch = ctx.gitBranches.get(tabId);
36
53
 
54
+ // Replay any tab-scoped events the web missed during the transport gap
55
+ // BEFORE tabInitialized so they arrive in the right order. Web-side
56
+ // handlers append; `tabInitialized` does NOT reset when `resumedFromSeq`
57
+ // is set, preserving the replayed additions.
58
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
59
+
37
60
  ctx.send(ws, {
38
61
  type: 'tabInitialized',
39
62
  tabId,
40
63
  data: {
41
64
  ...diskSession.getSessionInfo(),
42
- outputHistory: buildOutputHistory(diskSession),
65
+ ...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(diskSession) } : { resumedFromSeq: true }),
43
66
  ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
44
67
  }
45
68
  });
@@ -49,16 +72,17 @@ function tryResumeFromDisk(
49
72
  }
50
73
  }
51
74
 
52
- export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string, tabName?: string): Promise<void> {
75
+ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string, tabName?: string, rawData?: unknown): Promise<void> {
53
76
  const tabMap = ctx.connections.get(ws);
54
77
  const registry = ctx.getRegistry(workingDir);
78
+ const lastSeenSeq = extractLastSeenSeq(rawData);
55
79
 
56
80
  // 1. Check per-connection map (same WS reconnect)
57
81
  const existingSessionId = tabMap?.get(tabId);
58
82
  if (existingSessionId) {
59
83
  const existingSession = ctx.sessions.get(existingSessionId);
60
84
  if (existingSession) {
61
- reattachSession(ctx, existingSession, ws, tabId, registry);
85
+ reattachSession(ctx, existingSession, ws, tabId, registry, lastSeenSeq);
62
86
  return;
63
87
  }
64
88
  }
@@ -68,17 +92,25 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
68
92
  if (registrySessionId) {
69
93
  const inMemorySession = ctx.sessions.get(registrySessionId);
70
94
  if (inMemorySession) {
71
- reattachSession(ctx, inMemorySession, ws, tabId, registry);
95
+ reattachSession(ctx, inMemorySession, ws, tabId, registry, lastSeenSeq);
72
96
  return;
73
97
  }
74
98
 
75
- if (tryResumeFromDisk(ctx, ws, tabId, workingDir, registrySessionId, tabMap, registry)) {
99
+ if (tryResumeFromDisk(ctx, ws, tabId, workingDir, registrySessionId, tabMap, registry, lastSeenSeq)) {
76
100
  return;
77
101
  }
78
102
  }
79
103
 
80
- // 3. Create new session
81
- const session = new ImprovisationSessionManager({ workingDir, model: getModel(), effortLevel: getEffortLevel() });
104
+ // 3. Create new session. If the tab is already registered (no file on
105
+ // disk tab is pending first prompt or file was deleted), reuse its
106
+ // sessionId so the tab keeps its identity across restarts.
107
+ const existingTab = registry.getTab(tabId);
108
+ const session = new ImprovisationSessionManager({
109
+ workingDir,
110
+ ...(registrySessionId ? { sessionId: registrySessionId } : {}),
111
+ model: getModel(),
112
+ effortLevel: getEffortLevel(),
113
+ });
82
114
  setupSessionListeners(ctx, session, ws, tabId);
83
115
 
84
116
  const sessionId = session.getSessionInfo().sessionId;
@@ -88,17 +120,24 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
88
120
  tabMap.set(tabId, sessionId);
89
121
  }
90
122
 
91
- registry.registerTab(tabId, sessionId, tabName);
123
+ registry.registerTab(tabId, sessionId, tabName || existingTab?.tabName);
92
124
  const registeredTab = registry.getTab(tabId);
93
125
  ctx.broadcastToAll({
94
126
  type: 'tabCreated',
95
127
  data: { tabId, tabName: registeredTab?.tabName || 'Chat', createdAt: registeredTab?.createdAt, order: registeredTab?.order, sessionInfo: session.getSessionInfo() }
96
128
  });
97
129
 
130
+ // Fresh session (no disk/memory predecessor) has nothing to replay,
131
+ // but we still pass lastSeenSeq through so the web flag is consistent.
132
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
133
+
98
134
  ctx.send(ws, {
99
135
  type: 'tabInitialized',
100
136
  tabId,
101
- data: session.getSessionInfo()
137
+ data: {
138
+ ...session.getSessionInfo(),
139
+ ...(lastSeenSeq !== undefined ? { resumedFromSeq: true } : {}),
140
+ }
102
141
  });
103
142
  }
104
143
 
@@ -107,16 +146,18 @@ export async function resumeHistoricalSession(
107
146
  ws: WSContext,
108
147
  tabId: string,
109
148
  workingDir: string,
110
- historicalSessionId: string
149
+ historicalSessionId: string,
150
+ rawData?: unknown,
111
151
  ): Promise<void> {
112
152
  const tabMap = ctx.connections.get(ws);
113
153
  const registry = ctx.getRegistry(workingDir);
154
+ const lastSeenSeq = extractLastSeenSeq(rawData);
114
155
 
115
156
  const existingSessionId = tabMap?.get(tabId);
116
157
  if (existingSessionId) {
117
158
  const existingSession = ctx.sessions.get(existingSessionId);
118
159
  if (existingSession) {
119
- reattachSession(ctx, existingSession, ws, tabId, registry);
160
+ reattachSession(ctx, existingSession, ws, tabId, registry, lastSeenSeq);
120
161
  return;
121
162
  }
122
163
  }
@@ -125,7 +166,7 @@ export async function resumeHistoricalSession(
125
166
  if (registrySessionId) {
126
167
  const inMemorySession = ctx.sessions.get(registrySessionId);
127
168
  if (inMemorySession) {
128
- reattachSession(ctx, inMemorySession, ws, tabId, registry);
169
+ reattachSession(ctx, inMemorySession, ws, tabId, registry, lastSeenSeq);
129
170
  return;
130
171
  }
131
172
  }
@@ -152,12 +193,14 @@ export async function resumeHistoricalSession(
152
193
 
153
194
  registry.registerTab(tabId, sessionId);
154
195
 
196
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
197
+
155
198
  ctx.send(ws, {
156
199
  type: 'tabInitialized',
157
200
  tabId,
158
201
  data: {
159
202
  ...session.getSessionInfo(),
160
- outputHistory: buildOutputHistory(session),
203
+ ...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(session) } : { resumedFromSeq: true }),
161
204
  resumeFailed: isNewSession,
162
205
  originalSessionId: isNewSession ? historicalSessionId : undefined
163
206
  }
@@ -169,7 +212,8 @@ function reattachSession(
169
212
  session: ImprovisationSessionManager,
170
213
  ws: WSContext,
171
214
  tabId: string,
172
- registry: SessionRegistry
215
+ registry: SessionRegistry,
216
+ lastSeenSeq: number | undefined,
173
217
  ): void {
174
218
  setupSessionListeners(ctx, session, ws, tabId);
175
219
 
@@ -185,15 +229,35 @@ function reattachSession(
185
229
  if (regTab.worktreeBranch) ctx.gitBranches.set(tabId, regTab.worktreeBranch);
186
230
  }
187
231
 
188
- const outputHistory = buildOutputHistory(session);
232
+ const worktreePath = ctx.gitDirectories.get(tabId);
233
+ const worktreeBranch = ctx.gitBranches.get(tabId);
189
234
 
235
+ // Fast path: the web already has local state (via Zustand), so just replay
236
+ // anything newer than `lastSeenSeq` and tell the client to skip the
237
+ // destructive reset in its tabInitialized handler.
238
+ if (lastSeenSeq !== undefined) {
239
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
240
+ ctx.send(ws, {
241
+ type: 'tabInitialized',
242
+ tabId,
243
+ data: {
244
+ ...session.getSessionInfo(),
245
+ resumedFromSeq: true,
246
+ isExecuting: session.isExecuting,
247
+ ...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
248
+ ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
249
+ }
250
+ });
251
+ return;
252
+ }
253
+
254
+ // Cold-start reattach (no prior seq): send the full snapshot so the web
255
+ // can rebuild from scratch.
256
+ const outputHistory = buildOutputHistory(session);
190
257
  const executionEvents = session.isExecuting
191
258
  ? session.getExecutionEventLog()
192
259
  : undefined;
193
260
 
194
- const worktreePath = ctx.gitDirectories.get(tabId);
195
- const worktreeBranch = ctx.gitBranches.get(tabId);
196
-
197
261
  ctx.send(ws, {
198
262
  type: 'tabInitialized',
199
263
  tabId,