dmux 2.2.0 → 3.0.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 (197) hide show
  1. package/dist/DmuxApp.d.ts.map +1 -1
  2. package/dist/DmuxApp.js +412 -179
  3. package/dist/DmuxApp.js.map +1 -1
  4. package/dist/MergePane.d.ts.map +1 -1
  5. package/dist/MergePane.js +4 -15
  6. package/dist/MergePane.js.map +1 -1
  7. package/dist/PaneAnalyzer.d.ts +45 -0
  8. package/dist/PaneAnalyzer.d.ts.map +1 -0
  9. package/dist/PaneAnalyzer.js +278 -0
  10. package/dist/PaneAnalyzer.js.map +1 -0
  11. package/dist/_plugin-vue_export-helper.css +1 -0
  12. package/dist/actions/index.d.ts +19 -0
  13. package/dist/actions/index.d.ts.map +1 -0
  14. package/dist/actions/index.js +54 -0
  15. package/dist/actions/index.js.map +1 -0
  16. package/dist/actions/paneActions.d.ts +45 -0
  17. package/dist/actions/paneActions.d.ts.map +1 -0
  18. package/dist/actions/paneActions.js +932 -0
  19. package/dist/actions/paneActions.js.map +1 -0
  20. package/dist/actions/types.d.ts +101 -0
  21. package/dist/actions/types.d.ts.map +1 -0
  22. package/dist/actions/types.js +129 -0
  23. package/dist/actions/types.js.map +1 -0
  24. package/dist/adapters/apiActionHandler.d.ts +64 -0
  25. package/dist/adapters/apiActionHandler.d.ts.map +1 -0
  26. package/dist/adapters/apiActionHandler.js +170 -0
  27. package/dist/adapters/apiActionHandler.js.map +1 -0
  28. package/dist/adapters/tuiActionHandler.d.ts +57 -0
  29. package/dist/adapters/tuiActionHandler.d.ts.map +1 -0
  30. package/dist/adapters/tuiActionHandler.js +152 -0
  31. package/dist/adapters/tuiActionHandler.js.map +1 -0
  32. package/dist/chunks/_plugin-vue_export-helper-Cvoq67hi.js +28 -0
  33. package/dist/components/ActionChoiceDialog.d.ts +16 -0
  34. package/dist/components/ActionChoiceDialog.d.ts.map +1 -0
  35. package/dist/components/ActionChoiceDialog.js +29 -0
  36. package/dist/components/ActionChoiceDialog.js.map +1 -0
  37. package/dist/components/ActionConfirmDialog.d.ts +16 -0
  38. package/dist/components/ActionConfirmDialog.d.ts.map +1 -0
  39. package/dist/components/ActionConfirmDialog.js +31 -0
  40. package/dist/components/ActionConfirmDialog.js.map +1 -0
  41. package/dist/components/ActionInputDialog.d.ts +16 -0
  42. package/dist/components/ActionInputDialog.d.ts.map +1 -0
  43. package/dist/components/ActionInputDialog.js +49 -0
  44. package/dist/components/ActionInputDialog.js.map +1 -0
  45. package/dist/components/ActionProgressDialog.d.ts +13 -0
  46. package/dist/components/ActionProgressDialog.d.ts.map +1 -0
  47. package/dist/components/ActionProgressDialog.js +20 -0
  48. package/dist/components/ActionProgressDialog.js.map +1 -0
  49. package/dist/components/FooterHelp.d.ts +2 -0
  50. package/dist/components/FooterHelp.d.ts.map +1 -1
  51. package/dist/components/FooterHelp.js +9 -2
  52. package/dist/components/FooterHelp.js.map +1 -1
  53. package/dist/components/KebabMenu.d.ts +10 -0
  54. package/dist/components/KebabMenu.d.ts.map +1 -0
  55. package/dist/components/KebabMenu.js +18 -0
  56. package/dist/components/KebabMenu.js.map +1 -0
  57. package/dist/components/LoadingIndicator.d.ts.map +1 -1
  58. package/dist/components/LoadingIndicator.js +5 -5
  59. package/dist/components/LoadingIndicator.js.map +1 -1
  60. package/dist/components/PaneCard.d.ts +1 -0
  61. package/dist/components/PaneCard.d.ts.map +1 -1
  62. package/dist/components/PaneCard.js +21 -20
  63. package/dist/components/PaneCard.js.map +1 -1
  64. package/dist/components/PanesGrid.d.ts +1 -0
  65. package/dist/components/PanesGrid.d.ts.map +1 -1
  66. package/dist/components/PanesGrid.js +5 -4
  67. package/dist/components/PanesGrid.js.map +1 -1
  68. package/dist/components/QRCode.d.ts +7 -0
  69. package/dist/components/QRCode.d.ts.map +1 -0
  70. package/dist/components/QRCode.js +19 -0
  71. package/dist/components/QRCode.js.map +1 -0
  72. package/dist/components/Spinner.d.ts +10 -0
  73. package/dist/components/Spinner.d.ts.map +1 -0
  74. package/dist/components/Spinner.js +15 -0
  75. package/dist/components/Spinner.js.map +1 -0
  76. package/dist/dashboard.html +14 -0
  77. package/dist/dashboard.js +2 -0
  78. package/dist/hooks/useActionSystem.d.ts +31 -0
  79. package/dist/hooks/useActionSystem.d.ts.map +1 -0
  80. package/dist/hooks/useActionSystem.js +95 -0
  81. package/dist/hooks/useActionSystem.js.map +1 -0
  82. package/dist/hooks/useAgentStatus.d.ts +4 -3
  83. package/dist/hooks/useAgentStatus.d.ts.map +1 -1
  84. package/dist/hooks/useAgentStatus.js +45 -194
  85. package/dist/hooks/useAgentStatus.js.map +1 -1
  86. package/dist/hooks/usePaneCreation.d.ts +2 -1
  87. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  88. package/dist/hooks/usePaneCreation.js +46 -100
  89. package/dist/hooks/usePaneCreation.js.map +1 -1
  90. package/dist/hooks/usePanes.d.ts.map +1 -1
  91. package/dist/hooks/usePanes.js +5 -3
  92. package/dist/hooks/usePanes.js.map +1 -1
  93. package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
  94. package/dist/hooks/useTerminalWidth.js +18 -1
  95. package/dist/hooks/useTerminalWidth.js.map +1 -1
  96. package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
  97. package/dist/hooks/useWorktreeActions.js +4 -0
  98. package/dist/hooks/useWorktreeActions.js.map +1 -1
  99. package/dist/index.js +43 -6
  100. package/dist/index.js.map +1 -1
  101. package/dist/server/actionsApi.d.ts +37 -0
  102. package/dist/server/actionsApi.d.ts.map +1 -0
  103. package/dist/server/actionsApi.js +256 -0
  104. package/dist/server/actionsApi.js.map +1 -0
  105. package/dist/server/embedded-assets.d.ts +13 -0
  106. package/dist/server/embedded-assets.d.ts.map +1 -0
  107. package/dist/server/embedded-assets.js +5012 -0
  108. package/dist/server/embedded-assets.js.map +1 -0
  109. package/dist/server/index.d.ts +21 -0
  110. package/dist/server/index.d.ts.map +1 -0
  111. package/dist/server/index.js +99 -0
  112. package/dist/server/index.js.map +1 -0
  113. package/dist/server/routes.d.ts +3 -0
  114. package/dist/server/routes.d.ts.map +1 -0
  115. package/dist/server/routes.js +672 -0
  116. package/dist/server/routes.js.map +1 -0
  117. package/dist/server/static.d.ts +6 -0
  118. package/dist/server/static.d.ts.map +1 -0
  119. package/dist/server/static.js +3040 -0
  120. package/dist/server/static.js.map +1 -0
  121. package/dist/services/ConfigWatcher.d.ts +20 -0
  122. package/dist/services/ConfigWatcher.d.ts.map +1 -0
  123. package/dist/services/ConfigWatcher.js +75 -0
  124. package/dist/services/ConfigWatcher.js.map +1 -0
  125. package/dist/services/PaneWorkerManager.d.ts +69 -0
  126. package/dist/services/PaneWorkerManager.d.ts.map +1 -0
  127. package/dist/services/PaneWorkerManager.js +272 -0
  128. package/dist/services/PaneWorkerManager.js.map +1 -0
  129. package/dist/services/StatusDetector.d.ts +87 -0
  130. package/dist/services/StatusDetector.d.ts.map +1 -0
  131. package/dist/services/StatusDetector.js +387 -0
  132. package/dist/services/StatusDetector.js.map +1 -0
  133. package/dist/services/TerminalDiffer.d.ts +85 -0
  134. package/dist/services/TerminalDiffer.d.ts.map +1 -0
  135. package/dist/services/TerminalDiffer.js +499 -0
  136. package/dist/services/TerminalDiffer.js.map +1 -0
  137. package/dist/services/TerminalStreamer.d.ts +80 -0
  138. package/dist/services/TerminalStreamer.d.ts.map +1 -0
  139. package/dist/services/TerminalStreamer.js +490 -0
  140. package/dist/services/TerminalStreamer.js.map +1 -0
  141. package/dist/services/TunnelService.d.ts +9 -0
  142. package/dist/services/TunnelService.d.ts.map +1 -0
  143. package/dist/services/TunnelService.js +34 -0
  144. package/dist/services/TunnelService.js.map +1 -0
  145. package/dist/services/WorkerMessageBus.d.ts +48 -0
  146. package/dist/services/WorkerMessageBus.d.ts.map +1 -0
  147. package/dist/services/WorkerMessageBus.js +120 -0
  148. package/dist/services/WorkerMessageBus.js.map +1 -0
  149. package/dist/shared/StateManager.d.ts +34 -0
  150. package/dist/shared/StateManager.d.ts.map +1 -0
  151. package/dist/shared/StateManager.js +108 -0
  152. package/dist/shared/StateManager.js.map +1 -0
  153. package/dist/shared/StreamProtocol.d.ts +75 -0
  154. package/dist/shared/StreamProtocol.d.ts.map +1 -0
  155. package/dist/shared/StreamProtocol.js +37 -0
  156. package/dist/shared/StreamProtocol.js.map +1 -0
  157. package/dist/terminal.html +17 -0
  158. package/dist/terminal.js +3 -0
  159. package/dist/types.d.ts +21 -1
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/utils/agentDetection.d.ts +18 -0
  162. package/dist/utils/agentDetection.d.ts.map +1 -0
  163. package/dist/utils/agentDetection.js +73 -0
  164. package/dist/utils/agentDetection.js.map +1 -0
  165. package/dist/utils/aiMerge.d.ts +35 -0
  166. package/dist/utils/aiMerge.d.ts.map +1 -0
  167. package/dist/utils/aiMerge.js +298 -0
  168. package/dist/utils/aiMerge.js.map +1 -0
  169. package/dist/utils/conflictResolutionPane.d.ts +19 -0
  170. package/dist/utils/conflictResolutionPane.d.ts.map +1 -0
  171. package/dist/utils/conflictResolutionPane.js +214 -0
  172. package/dist/utils/conflictResolutionPane.js.map +1 -0
  173. package/dist/utils/mergeExecution.d.ts +52 -0
  174. package/dist/utils/mergeExecution.d.ts.map +1 -0
  175. package/dist/utils/mergeExecution.js +192 -0
  176. package/dist/utils/mergeExecution.js.map +1 -0
  177. package/dist/utils/mergeValidation.d.ts +67 -0
  178. package/dist/utils/mergeValidation.d.ts.map +1 -0
  179. package/dist/utils/mergeValidation.js +213 -0
  180. package/dist/utils/mergeValidation.js.map +1 -0
  181. package/dist/utils/paneCreation.d.ts +17 -0
  182. package/dist/utils/paneCreation.d.ts.map +1 -0
  183. package/dist/utils/paneCreation.js +274 -0
  184. package/dist/utils/paneCreation.js.map +1 -0
  185. package/dist/utils/port.d.ts +5 -0
  186. package/dist/utils/port.d.ts.map +1 -0
  187. package/dist/utils/port.js +54 -0
  188. package/dist/utils/port.js.map +1 -0
  189. package/dist/workers/PaneWorker.d.ts +2 -0
  190. package/dist/workers/PaneWorker.d.ts.map +1 -0
  191. package/dist/workers/PaneWorker.js +362 -0
  192. package/dist/workers/PaneWorker.js.map +1 -0
  193. package/dist/workers/WorkerMessages.d.ts +36 -0
  194. package/dist/workers/WorkerMessages.d.ts.map +1 -0
  195. package/dist/workers/WorkerMessages.js +9 -0
  196. package/dist/workers/WorkerMessages.js.map +1 -0
  197. package/package.json +19 -5
@@ -0,0 +1,672 @@
1
+ import { eventHandler, getRouterParams, readBody, setHeader, createEventStream, createRouter } from 'h3';
2
+ import { StateManager } from '../shared/StateManager.js';
3
+ import { getEmbeddedAsset } from './embedded-assets.js';
4
+ function serveEmbeddedAsset(filename) {
5
+ const asset = getEmbeddedAsset(filename);
6
+ if (!asset) {
7
+ throw new Error(`Embedded asset not found: ${filename}`);
8
+ }
9
+ return asset.content;
10
+ }
11
+ import { getTerminalStreamer } from '../services/TerminalStreamer.js';
12
+ import { formatStreamMessage } from '../shared/StreamProtocol.js';
13
+ import { handleListActions, handleGetPaneActions, handleExecuteAction, handleConfirmCallback, handleChoiceCallback, handleInputCallback } from './actionsApi.js';
14
+ const stateManager = StateManager.getInstance();
15
+ export function setupRoutes(app) {
16
+ // CORS middleware for all routes
17
+ app.use('/', eventHandler(async (event) => {
18
+ setHeader(event, 'Access-Control-Allow-Origin', '*');
19
+ setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
20
+ setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type');
21
+ if (event.node.req.method === 'OPTIONS') {
22
+ event.node.res.statusCode = 204;
23
+ return '';
24
+ }
25
+ }));
26
+ // GET /api/health - Health check
27
+ app.use('/api/health', eventHandler(async () => {
28
+ return { status: 'ok', timestamp: Date.now() };
29
+ }));
30
+ // GET /api/session - Get session info
31
+ app.use('/api/session', eventHandler(async () => {
32
+ const state = stateManager.getState();
33
+ return {
34
+ projectName: state.projectName,
35
+ sessionName: state.sessionName,
36
+ projectRoot: state.projectRoot,
37
+ serverUrl: state.serverUrl,
38
+ settings: state.settings,
39
+ paneCount: state.panes.length,
40
+ timestamp: Date.now()
41
+ };
42
+ }));
43
+ // ===== Action System Routes =====
44
+ // Create a router for exact route matching
45
+ const apiRouter = createRouter();
46
+ // GET /api/actions - List all actions
47
+ apiRouter.get('/api/actions', eventHandler(async (event) => {
48
+ return new Promise((resolve, reject) => {
49
+ handleListActions(event.node.req, {
50
+ writeHead: (code, headers) => {
51
+ event.node.res.statusCode = code;
52
+ Object.entries(headers).forEach(([k, v]) => setHeader(event, k, v));
53
+ },
54
+ end: (data) => resolve(JSON.parse(data))
55
+ });
56
+ });
57
+ }));
58
+ // GET /api/panes/:id/actions - Get available actions for pane
59
+ apiRouter.get('/api/panes/:id/actions', eventHandler(async (event) => {
60
+ const params = getRouterParams(event);
61
+ const paneId = params?.id;
62
+ if (!paneId) {
63
+ event.node.res.statusCode = 400;
64
+ return { error: 'Missing pane ID' };
65
+ }
66
+ return new Promise((resolve, reject) => {
67
+ handleGetPaneActions(event.node.req, {
68
+ writeHead: (code, headers) => {
69
+ event.node.res.statusCode = code;
70
+ Object.entries(headers).forEach(([k, v]) => setHeader(event, k, v));
71
+ },
72
+ end: (data) => resolve(JSON.parse(data))
73
+ }, decodeURIComponent(paneId));
74
+ });
75
+ }));
76
+ // POST /api/panes/:id/actions/:actionId - Execute action
77
+ apiRouter.post('/api/panes/:paneId/actions/:actionId', eventHandler(async (event) => {
78
+ const params = getRouterParams(event);
79
+ const paneId = params?.paneId;
80
+ const actionId = params?.actionId;
81
+ if (!paneId || !actionId) {
82
+ event.node.res.statusCode = 400;
83
+ return { error: 'Missing pane ID or action ID' };
84
+ }
85
+ return new Promise((resolve, reject) => {
86
+ handleExecuteAction(event.node.req, {
87
+ writeHead: (code, headers) => {
88
+ event.node.res.statusCode = code;
89
+ Object.entries(headers).forEach(([k, v]) => setHeader(event, k, v));
90
+ },
91
+ end: (data) => resolve(JSON.parse(data))
92
+ }, decodeURIComponent(paneId), decodeURIComponent(actionId));
93
+ });
94
+ }));
95
+ // POST /api/callbacks/confirm/:callbackId - Respond to confirm dialog
96
+ apiRouter.post('/api/callbacks/confirm/:callbackId', eventHandler(async (event) => {
97
+ const params = getRouterParams(event);
98
+ const callbackId = params?.callbackId;
99
+ if (!callbackId) {
100
+ event.node.res.statusCode = 400;
101
+ return { error: 'Missing callback ID' };
102
+ }
103
+ return new Promise((resolve, reject) => {
104
+ handleConfirmCallback(event.node.req, {
105
+ writeHead: (code, headers) => {
106
+ event.node.res.statusCode = code;
107
+ Object.entries(headers).forEach(([k, v]) => setHeader(event, k, v));
108
+ },
109
+ end: (data) => resolve(JSON.parse(data))
110
+ }, decodeURIComponent(callbackId));
111
+ });
112
+ }));
113
+ // POST /api/callbacks/choice/:callbackId - Respond to choice dialog
114
+ apiRouter.post('/api/callbacks/choice/:callbackId', eventHandler(async (event) => {
115
+ const params = getRouterParams(event);
116
+ const callbackId = params?.callbackId;
117
+ if (!callbackId) {
118
+ event.node.res.statusCode = 400;
119
+ return { error: 'Missing callback ID' };
120
+ }
121
+ return new Promise((resolve, reject) => {
122
+ handleChoiceCallback(event.node.req, {
123
+ writeHead: (code, headers) => {
124
+ event.node.res.statusCode = code;
125
+ Object.entries(headers).forEach(([k, v]) => setHeader(event, k, v));
126
+ },
127
+ end: (data) => resolve(JSON.parse(data))
128
+ }, decodeURIComponent(callbackId));
129
+ });
130
+ }));
131
+ // POST /api/callbacks/input/:callbackId - Respond to input dialog
132
+ apiRouter.post('/api/callbacks/input/:callbackId', eventHandler(async (event) => {
133
+ const params = getRouterParams(event);
134
+ const callbackId = params?.callbackId;
135
+ if (!callbackId) {
136
+ event.node.res.statusCode = 400;
137
+ return { error: 'Missing callback ID' };
138
+ }
139
+ return new Promise((resolve, reject) => {
140
+ handleInputCallback(event.node.req, {
141
+ writeHead: (code, headers) => {
142
+ event.node.res.statusCode = code;
143
+ Object.entries(headers).forEach(([k, v]) => setHeader(event, k, v));
144
+ },
145
+ end: (data) => resolve(JSON.parse(data))
146
+ }, decodeURIComponent(callbackId));
147
+ });
148
+ }));
149
+ // GET /api/panes/:id/snapshot - Get current pane snapshot
150
+ apiRouter.get('/api/panes/:id/snapshot', eventHandler(async (event) => {
151
+ const params = getRouterParams(event);
152
+ const paneId = params?.id;
153
+ if (!paneId) {
154
+ event.node.res.statusCode = 400;
155
+ return { error: 'Missing pane ID' };
156
+ }
157
+ const pane = stateManager.getPaneById(decodeURIComponent(paneId));
158
+ if (!pane) {
159
+ event.node.res.statusCode = 404;
160
+ return { error: 'Pane not found' };
161
+ }
162
+ // Capture current pane state from tmux
163
+ const { execSync } = await import('child_process');
164
+ try {
165
+ // Get dimensions
166
+ const dimensionsOutput = execSync(`tmux display-message -p -t ${pane.paneId} -F "#{pane_width},#{pane_height}"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
167
+ const [width, height] = dimensionsOutput.split(',').map(Number);
168
+ // Get content
169
+ const content = execSync(`tmux capture-pane -epJ -t ${pane.paneId}`, { encoding: 'utf-8', stdio: 'pipe' });
170
+ // Get cursor position
171
+ const cursorOutput = execSync(`tmux display-message -p -t ${pane.paneId} -F "#{cursor_y},#{cursor_x}"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
172
+ const [cursorRow, cursorCol] = cursorOutput.split(',').map(Number);
173
+ return {
174
+ width: width || 80,
175
+ height: height || 24,
176
+ content,
177
+ cursorRow: cursorRow || 0,
178
+ cursorCol: cursorCol || 0
179
+ };
180
+ }
181
+ catch (error) {
182
+ event.node.res.statusCode = 500;
183
+ return { error: 'Failed to capture pane state' };
184
+ }
185
+ }));
186
+ // POST /api/panes/:id/actions - Execute action on pane
187
+ apiRouter.post('/api/panes/:id/actions', eventHandler(async (event) => {
188
+ const params = getRouterParams(event);
189
+ const paneId = params?.id;
190
+ if (!paneId) {
191
+ event.node.res.statusCode = 400;
192
+ return { error: 'Missing pane ID' };
193
+ }
194
+ const pane = stateManager.getPaneById(decodeURIComponent(paneId));
195
+ if (!pane) {
196
+ event.node.res.statusCode = 404;
197
+ return { error: 'Pane not found' };
198
+ }
199
+ try {
200
+ const action = await readBody(event);
201
+ // For now, just acknowledge the action
202
+ // Future: Implement actual pane actions (sendKeys, resize, etc.)
203
+ return {
204
+ status: 'acknowledged',
205
+ paneId,
206
+ action,
207
+ message: 'Action endpoints will be implemented in future versions'
208
+ };
209
+ }
210
+ catch (err) {
211
+ event.node.res.statusCode = 400;
212
+ return { error: 'Invalid request body' };
213
+ }
214
+ }));
215
+ // GET /api/panes/:id - Get specific pane
216
+ apiRouter.get('/api/panes/:id', eventHandler(async (event) => {
217
+ const params = getRouterParams(event);
218
+ const paneId = params?.id;
219
+ if (!paneId) {
220
+ event.node.res.statusCode = 400;
221
+ return { error: 'Missing pane ID' };
222
+ }
223
+ const pane = stateManager.getPaneById(decodeURIComponent(paneId));
224
+ if (!pane) {
225
+ event.node.res.statusCode = 404;
226
+ return { error: 'Pane not found' };
227
+ }
228
+ return formatPaneResponse(pane);
229
+ }));
230
+ // GET /api/panes - List all panes
231
+ apiRouter.get('/api/panes', eventHandler(async (event) => {
232
+ const state = stateManager.getState();
233
+ return {
234
+ panes: state.panes.map(formatPaneResponse),
235
+ projectName: state.projectName,
236
+ sessionName: state.sessionName,
237
+ timestamp: Date.now()
238
+ };
239
+ }));
240
+ // GET /api/panes-stream - Stream pane updates via SSE
241
+ apiRouter.get('/api/panes-stream', eventHandler(async (event) => {
242
+ const eventStream = createEventStream(event);
243
+ // Send initial state
244
+ const initialState = stateManager.getState();
245
+ eventStream.push(JSON.stringify({
246
+ type: 'init',
247
+ data: {
248
+ panes: initialState.panes.map(formatPaneResponse),
249
+ projectName: initialState.projectName,
250
+ sessionName: initialState.sessionName,
251
+ timestamp: Date.now()
252
+ }
253
+ }));
254
+ // Subscribe to state changes
255
+ const unsubscribe = stateManager.subscribe((state) => {
256
+ try {
257
+ eventStream.push(JSON.stringify({
258
+ type: 'update',
259
+ data: {
260
+ panes: state.panes.map(formatPaneResponse),
261
+ projectName: state.projectName,
262
+ sessionName: state.sessionName,
263
+ timestamp: Date.now()
264
+ }
265
+ }));
266
+ }
267
+ catch (err) {
268
+ console.error('Failed to send SSE update:', err);
269
+ }
270
+ });
271
+ // Send heartbeat every 30 seconds
272
+ const heartbeat = setInterval(() => {
273
+ try {
274
+ eventStream.push(JSON.stringify({
275
+ type: 'heartbeat',
276
+ timestamp: Date.now()
277
+ }));
278
+ }
279
+ catch {
280
+ clearInterval(heartbeat);
281
+ }
282
+ }, 30000);
283
+ // Cleanup on disconnect
284
+ event.node.req.on('close', () => {
285
+ clearInterval(heartbeat);
286
+ unsubscribe();
287
+ eventStream.close();
288
+ });
289
+ return eventStream.send();
290
+ }));
291
+ // POST /api/panes - Create a new pane
292
+ apiRouter.post('/api/panes', eventHandler(async (event) => {
293
+ try {
294
+ const body = await readBody(event);
295
+ let { prompt, agent } = body;
296
+ console.error('[API] POST /api/panes called with:', { prompt, agent, body });
297
+ if (!prompt || typeof prompt !== 'string') {
298
+ event.node.res.statusCode = 400;
299
+ return { error: 'Missing or invalid prompt' };
300
+ }
301
+ // Normalize agent to undefined if not provided or empty
302
+ if (!agent || agent === '') {
303
+ agent = undefined;
304
+ }
305
+ console.error('[API] After normalization, agent =', agent);
306
+ if (agent && agent !== 'claude' && agent !== 'opencode') {
307
+ event.node.res.statusCode = 400;
308
+ return { error: 'Invalid agent. Must be "claude" or "opencode"' };
309
+ }
310
+ // Get available agents using robust detection (same as TUI)
311
+ const { execSync } = await import('child_process');
312
+ const fsPromises = await import('fs/promises');
313
+ const availableAgents = [];
314
+ // Check for Claude
315
+ const hasClaude = await (async () => {
316
+ try {
317
+ const userShell = process.env.SHELL || '/bin/bash';
318
+ const result = execSync(`${userShell} -i -c "command -v claude 2>/dev/null || which claude 2>/dev/null"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
319
+ if (result)
320
+ return true;
321
+ }
322
+ catch { }
323
+ const claudePaths = [
324
+ `${process.env.HOME}/.claude/local/claude`,
325
+ `${process.env.HOME}/.local/bin/claude`,
326
+ '/usr/local/bin/claude',
327
+ '/opt/homebrew/bin/claude',
328
+ '/usr/bin/claude',
329
+ `${process.env.HOME}/bin/claude`,
330
+ ];
331
+ for (const p of claudePaths) {
332
+ try {
333
+ await fsPromises.access(p);
334
+ return true;
335
+ }
336
+ catch { }
337
+ }
338
+ return false;
339
+ })();
340
+ if (hasClaude)
341
+ availableAgents.push('claude');
342
+ // Check for opencode
343
+ const hasOpencode = await (async () => {
344
+ try {
345
+ const userShell = process.env.SHELL || '/bin/bash';
346
+ const result = execSync(`${userShell} -i -c "command -v opencode 2>/dev/null || which opencode 2>/dev/null"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
347
+ if (result)
348
+ return true;
349
+ }
350
+ catch { }
351
+ const opencodePaths = [
352
+ '/opt/homebrew/bin/opencode',
353
+ '/usr/local/bin/opencode',
354
+ `${process.env.HOME}/.local/bin/opencode`,
355
+ `${process.env.HOME}/bin/opencode`,
356
+ ];
357
+ for (const p of opencodePaths) {
358
+ try {
359
+ await fsPromises.access(p);
360
+ return true;
361
+ }
362
+ catch { }
363
+ }
364
+ return false;
365
+ })();
366
+ if (hasOpencode)
367
+ availableAgents.push('opencode');
368
+ console.error('[API] Available agents:', availableAgents);
369
+ if (availableAgents.length === 0) {
370
+ event.node.res.statusCode = 500;
371
+ return { error: 'No agents available. Install claude or opencode.' };
372
+ }
373
+ // If no agent specified and multiple available, return agent choice needed
374
+ if (!agent && availableAgents.length > 1) {
375
+ console.error('[API] Returning needsAgentChoice');
376
+ return {
377
+ needsAgentChoice: true,
378
+ availableAgents,
379
+ message: 'Please specify an agent (claude or opencode) in the request body',
380
+ };
381
+ }
382
+ console.error('[API] Proceeding to create pane with agent:', agent);
383
+ // Import pane creation utility
384
+ const { createPane } = await import('../utils/paneCreation.js');
385
+ const state = stateManager.getState();
386
+ // Create the pane
387
+ const result = await createPane({
388
+ prompt,
389
+ agent: agent || (availableAgents.length === 1 ? availableAgents[0] : undefined),
390
+ projectName: state.projectName || 'unknown',
391
+ existingPanes: state.panes,
392
+ }, availableAgents);
393
+ // Check if agent choice is needed
394
+ if (result.needsAgentChoice) {
395
+ return {
396
+ needsAgentChoice: true,
397
+ availableAgents,
398
+ message: 'Please specify an agent (claude or opencode) in the request body',
399
+ };
400
+ }
401
+ // Save the new pane to the panes file
402
+ const fs = await import('fs/promises');
403
+ const path = await import('path');
404
+ // Get panes file path from state
405
+ const projectRoot = state.projectRoot || process.cwd();
406
+ const panesFile = path.join(projectRoot, '.dmux', `dmux.config.json`);
407
+ // Read existing panes
408
+ let existingPanes = [];
409
+ try {
410
+ const configContent = await fs.readFile(panesFile, 'utf-8');
411
+ const config = JSON.parse(configContent);
412
+ existingPanes = Array.isArray(config) ? config : config.panes || [];
413
+ }
414
+ catch {
415
+ // File doesn't exist yet, start with empty array
416
+ }
417
+ // Add new pane
418
+ const updatedPanes = [...existingPanes, result.pane];
419
+ // Write back to file - ConfigWatcher will update StateManager
420
+ await fs.writeFile(panesFile, JSON.stringify({ panes: updatedPanes }, null, 2));
421
+ return {
422
+ success: true,
423
+ pane: formatPaneResponse(result.pane),
424
+ message: 'Pane created successfully',
425
+ };
426
+ }
427
+ catch (err) {
428
+ console.error('Failed to create pane:', err);
429
+ event.node.res.statusCode = 500;
430
+ return { error: 'Failed to create pane', details: err.message };
431
+ }
432
+ }));
433
+ // Mount the API router
434
+ app.use(apiRouter);
435
+ // GET /api/stream/:paneId - Stream terminal output
436
+ // MUST be before the catch-all route
437
+ app.use('/api/stream', eventHandler(async (event) => {
438
+ const url = event.node.req.url || '';
439
+ // When using app.use('/api/stream'), the URL is already stripped
440
+ // So we just need to remove the leading slash
441
+ const dmuxId = url.startsWith('/') ? url.substring(1) : url;
442
+ if (!dmuxId || dmuxId.includes('/')) {
443
+ event.node.res.statusCode = 404;
444
+ return { error: 'Invalid pane ID' };
445
+ }
446
+ // Find the pane by dmux ID
447
+ const pane = stateManager.getPaneById(decodeURIComponent(dmuxId));
448
+ if (!pane || !pane.paneId) {
449
+ event.node.res.statusCode = 404;
450
+ return { error: 'Pane not found' };
451
+ }
452
+ // Create a readable stream
453
+ const { Readable } = await import('stream');
454
+ const stream = new Readable({
455
+ read() { } // No-op, we'll push data as it arrives
456
+ });
457
+ const streamer = getTerminalStreamer();
458
+ // Start streaming - pass the stream object
459
+ await streamer.startStream(pane.id, pane.paneId, stream);
460
+ // Handle client disconnect
461
+ event.node.req.on('close', () => {
462
+ streamer.stopStream(pane.id, stream);
463
+ stream.destroy();
464
+ });
465
+ // Send heartbeat every 30 seconds to keep connection alive
466
+ const heartbeat = setInterval(() => {
467
+ try {
468
+ const heartbeatMessage = {
469
+ type: 'heartbeat',
470
+ timestamp: Date.now()
471
+ };
472
+ stream.push(formatStreamMessage(heartbeatMessage));
473
+ }
474
+ catch {
475
+ clearInterval(heartbeat);
476
+ }
477
+ }, 30000);
478
+ // Cleanup heartbeat on disconnect
479
+ event.node.req.on('close', () => {
480
+ clearInterval(heartbeat);
481
+ stream.destroy();
482
+ });
483
+ // Return the readable stream - h3 will pipe it to the response
484
+ return stream;
485
+ }));
486
+ // POST /api/keys/:paneId - Send keystrokes to pane
487
+ app.use('/api/keys', eventHandler(async (event) => {
488
+ // Only accept POST requests
489
+ if (event.node.req.method !== 'POST') {
490
+ event.node.res.statusCode = 405;
491
+ return { error: 'Method not allowed' };
492
+ }
493
+ const url = event.node.req.url || '';
494
+ const dmuxId = url.startsWith('/') ? url.substring(1) : url;
495
+ if (!dmuxId || dmuxId.includes('/')) {
496
+ event.node.res.statusCode = 404;
497
+ return { error: 'Invalid pane ID' };
498
+ }
499
+ // Find the pane by dmux ID
500
+ const pane = stateManager.getPaneById(decodeURIComponent(dmuxId));
501
+ if (!pane || !pane.paneId) {
502
+ event.node.res.statusCode = 404;
503
+ return { error: 'Pane not found' };
504
+ }
505
+ // Read the keystroke data from the request body
506
+ const body = await readBody(event);
507
+ if (!body || typeof body.key !== 'string') {
508
+ event.node.res.statusCode = 400;
509
+ return { error: 'Missing or invalid key data' };
510
+ }
511
+ try {
512
+ const { execSync } = await import('child_process');
513
+ // Map special keys to tmux send-keys format
514
+ const key = body.key;
515
+ let tmuxKey = key;
516
+ // Handle special keys
517
+ const specialKeys = {
518
+ 'Enter': 'Enter',
519
+ 'Tab': 'Tab',
520
+ 'Backspace': 'BSpace',
521
+ 'Delete': 'DC', // Delete Character
522
+ 'Escape': 'Escape',
523
+ 'ArrowUp': 'Up',
524
+ 'ArrowDown': 'Down',
525
+ 'ArrowLeft': 'Left',
526
+ 'ArrowRight': 'Right',
527
+ 'Home': 'Home',
528
+ 'End': 'End',
529
+ 'PageUp': 'PageUp',
530
+ 'PageDown': 'PageDown',
531
+ 'Space': 'Space'
532
+ };
533
+ // Priority order: Ctrl/Alt combinations first, then special keys, then regular
534
+ // Handle Ctrl+ combinations with regular characters
535
+ if (body.ctrlKey && key.length === 1 && !specialKeys[key]) {
536
+ tmuxKey = `C-${key.toLowerCase()}`;
537
+ }
538
+ // Handle Alt+ combinations with regular characters
539
+ else if (body.altKey && key.length === 1 && !specialKeys[key]) {
540
+ tmuxKey = `M-${key.toLowerCase()}`;
541
+ }
542
+ // Handle Shift+Tab
543
+ else if (body.shiftKey && key === 'Tab') {
544
+ tmuxKey = 'BTab';
545
+ }
546
+ // Handle Shift+Enter - send the escape sequence using printf to handle escape character
547
+ else if (body.shiftKey && key === 'Enter') {
548
+ // Send ESC[13;2~ which is the standard Shift+Enter sequence
549
+ execSync(`printf '\\033[13;2~' | tmux load-buffer - && tmux paste-buffer -t ${pane.paneId}`, {
550
+ stdio: 'pipe',
551
+ shell: '/bin/bash'
552
+ });
553
+ return { success: true, key: 'Shift+Enter (CSI sequence)' };
554
+ }
555
+ // Handle Ctrl+ with special keys
556
+ else if (body.ctrlKey && specialKeys[key]) {
557
+ tmuxKey = `C-${specialKeys[key]}`;
558
+ }
559
+ // Handle Alt+ with special keys
560
+ else if (body.altKey && specialKeys[key]) {
561
+ tmuxKey = `M-${specialKeys[key]}`;
562
+ }
563
+ // Handle special keys alone
564
+ else if (specialKeys[key]) {
565
+ tmuxKey = specialKeys[key];
566
+ }
567
+ // Regular character - use -l flag to send literally
568
+ else if (key.length === 1) {
569
+ execSync(`tmux send-keys -t ${pane.paneId} -l ${JSON.stringify(key)}`, {
570
+ stdio: 'pipe'
571
+ });
572
+ return { success: true, key: key };
573
+ }
574
+ // Send the key to tmux (for all non-literal keys)
575
+ execSync(`tmux send-keys -t ${pane.paneId} ${tmuxKey}`, {
576
+ stdio: 'pipe'
577
+ });
578
+ return { success: true, key: tmuxKey };
579
+ }
580
+ catch (error) {
581
+ console.error('Failed to send keys to pane:', error);
582
+ event.node.res.statusCode = 500;
583
+ return { error: 'Failed to send keys', details: error.message };
584
+ }
585
+ }));
586
+ // GET /api/stream-stats - Get streaming statistics
587
+ app.use('/api/stream-stats', eventHandler(async () => {
588
+ const streamer = getTerminalStreamer();
589
+ return streamer.getStats();
590
+ }));
591
+ // GET /api/test-stream - Simple test stream
592
+ app.use('/api/test-stream', eventHandler(async (event) => {
593
+ const { Readable } = await import('stream');
594
+ const stream = new Readable({
595
+ read() { }
596
+ });
597
+ // Send some test data
598
+ stream.push('TEST:First message\n');
599
+ setTimeout(() => stream.push('TEST:Second message\n'), 100);
600
+ setTimeout(() => stream.push('TEST:Third message\n'), 200);
601
+ setTimeout(() => stream.push(null), 300); // End stream
602
+ return stream;
603
+ }));
604
+ // Static files - Dashboard HTML
605
+ // IMPORTANT: This must be last to avoid catching API routes
606
+ app.use('/', eventHandler(async (event) => {
607
+ const path = event.node.req.url || '/';
608
+ // Skip API routes - let them 404 naturally
609
+ if (path.startsWith('/api/')) {
610
+ event.node.res.statusCode = 404;
611
+ return { error: 'API endpoint not found' };
612
+ }
613
+ if (path === '/' || path === '/index.html') {
614
+ setHeader(event, 'Content-Type', 'text/html');
615
+ return serveEmbeddedAsset('dashboard.html');
616
+ }
617
+ // Terminal viewer page
618
+ if (path.startsWith('/panes/')) {
619
+ setHeader(event, 'Content-Type', 'text/html');
620
+ return serveEmbeddedAsset('terminal.html');
621
+ }
622
+ // Serve any CSS file from root of dist/
623
+ if (path.endsWith('.css')) {
624
+ const filename = path.substring(1); // Remove leading /
625
+ const asset = getEmbeddedAsset(filename);
626
+ if (asset) {
627
+ setHeader(event, 'Content-Type', 'text/css');
628
+ return asset.content;
629
+ }
630
+ }
631
+ // Serve any JS file from root of dist/ (not in subdirectories)
632
+ if (path.endsWith('.js') && path.lastIndexOf('/') === 0) {
633
+ const filename = path.substring(1); // Remove leading /
634
+ const asset = getEmbeddedAsset(filename);
635
+ if (asset) {
636
+ setHeader(event, 'Content-Type', 'application/javascript');
637
+ return asset.content;
638
+ }
639
+ }
640
+ // Serve chunk files
641
+ if (path.startsWith('/chunks/')) {
642
+ setHeader(event, 'Content-Type', 'application/javascript');
643
+ const filename = path.substring(1); // Remove leading /
644
+ return serveEmbeddedAsset(filename);
645
+ }
646
+ // 404 for unknown routes
647
+ event.node.res.statusCode = 404;
648
+ return 'Not Found';
649
+ }));
650
+ }
651
+ function formatPaneResponse(pane) {
652
+ return {
653
+ id: pane.id,
654
+ slug: pane.slug,
655
+ prompt: pane.prompt,
656
+ paneId: pane.paneId,
657
+ worktreePath: pane.worktreePath,
658
+ agent: pane.agent || 'unknown',
659
+ agentStatus: pane.agentStatus || 'idle',
660
+ testStatus: pane.testStatus,
661
+ testWindowId: pane.testWindowId,
662
+ devStatus: pane.devStatus,
663
+ devUrl: pane.devUrl,
664
+ devWindowId: pane.devWindowId,
665
+ lastAgentCheck: pane.lastAgentCheck,
666
+ optionsQuestion: pane.optionsQuestion,
667
+ options: pane.options,
668
+ potentialHarm: pane.potentialHarm,
669
+ agentSummary: pane.agentSummary
670
+ };
671
+ }
672
+ //# sourceMappingURL=routes.js.map