@mmmbuto/nexuscrew 0.2.1 → 0.2.3

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 (52) hide show
  1. package/README.md +122 -67
  2. package/bin/nexuscrew.js +194 -470
  3. package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +3 -11
  6. package/lib/server/routes/hosts.js +22 -12
  7. package/lib/server/routes/launchers.js +12 -0
  8. package/lib/server/routes/models.js +16 -63
  9. package/lib/server/routes/runtime.js +10 -0
  10. package/lib/server/routes/send.js +97 -258
  11. package/lib/server/routes/sessions.js +13 -42
  12. package/lib/server/routes/status.js +18 -22
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +60 -81
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/log-watcher.js +22 -40
  17. package/lib/services/session-store.js +88 -62
  18. package/lib/services/tmux-manager.js +111 -173
  19. package/package.json +6 -10
  20. package/frontend/dist/apple-touch-icon.png +0 -0
  21. package/frontend/dist/assets/index-CiAtinNP.css +0 -1
  22. package/frontend/dist/favicon.svg +0 -20
  23. package/frontend/dist/icon-192.png +0 -0
  24. package/frontend/dist/icon-512.png +0 -0
  25. package/frontend/dist/site.webmanifest +0 -29
  26. package/lib/config/hosts.js +0 -67
  27. package/lib/config/manager.js +0 -379
  28. package/lib/config/models.js +0 -408
  29. package/lib/server/db/adapter.js +0 -274
  30. package/lib/server/db/drivers/sql-js.js +0 -75
  31. package/lib/server/db/migrate.js +0 -174
  32. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  33. package/lib/server/middleware/auth.js +0 -134
  34. package/lib/server/middleware/rate-limit.js +0 -63
  35. package/lib/server/models/User.js +0 -128
  36. package/lib/server/routes/auth.js +0 -168
  37. package/lib/server/routes/keys.js +0 -28
  38. package/lib/server/routes/runtimes.js +0 -34
  39. package/lib/server/routes/speech.js +0 -46
  40. package/lib/server/routes/upload.js +0 -135
  41. package/lib/server/routes/wake-lock.js +0 -95
  42. package/lib/server/routes/workspaces.js +0 -101
  43. package/lib/server/services/attachment-manager.js +0 -57
  44. package/lib/server/services/context-bridge.js +0 -425
  45. package/lib/server/services/runtime-manager.js +0 -462
  46. package/lib/server/services/speech-manager.js +0 -76
  47. package/lib/server/services/summary-generator.js +0 -309
  48. package/lib/server/services/workspace-manager.js +0 -79
  49. package/lib/services/remote-pane-watcher.js +0 -155
  50. package/lib/setup/postinstall.js +0 -38
  51. package/lib/utils/paths.js +0 -124
  52. package/lib/utils/termux.js +0 -182
@@ -1,39 +1,33 @@
1
1
  /**
2
- * POST /api/v1/send — Unified send endpoint
2
+ * POST /api/v1/send — send a message to an existing tmux session
3
+ * POST /api/v1/send/interrupt — interrupt current generation
3
4
  *
4
- * Routes message to the correct tmux window.
5
- * Creates new window if needed, resumes existing.
5
+ * This route never creates tmux sessions implicitly.
6
6
  */
7
7
 
8
8
  const { Router } = require('express');
9
- const { v4: uuidv4 } = require('uuid');
10
- const path = require('path');
9
+ const os = require('os');
11
10
 
12
11
  const router = Router();
13
12
 
14
13
  router.post('/', async (req, res) => {
15
- const { tmux, engines, store, logWatcher, remotePaneWatcher, attachments: attachmentManager } = req;
14
+ const { tmux, store, logWatcher } = req;
16
15
  const {
17
16
  conversationId,
18
17
  message,
19
- attachments = [],
20
- model,
21
- engine: engineOverride,
18
+ tmuxSession,
22
19
  workspace,
23
- host: hostName,
24
- alias,
25
- resumeSession
20
+ host: hostName
26
21
  } = req.body;
27
22
 
28
23
  if (!message || !message.trim()) {
29
24
  return res.status(400).json({ error: 'Message is required' });
30
25
  }
31
26
 
32
- // Set SSE headers
33
27
  res.writeHead(200, {
34
28
  'Content-Type': 'text/event-stream',
35
29
  'Cache-Control': 'no-cache',
36
- 'Connection': 'keep-alive',
30
+ Connection: 'keep-alive',
37
31
  'X-Accel-Buffering': 'no'
38
32
  });
39
33
 
@@ -42,232 +36,127 @@ router.post('/', async (req, res) => {
42
36
  };
43
37
 
44
38
  try {
45
- // Resolve engine and model
46
- const resolvedEngine = engineOverride || resolveEngine(model, engines);
47
- const host = hostName || 'local';
48
- const hostObj = tmux.resolveHost(host);
49
- if (!hostObj) {
50
- throw new Error(`Unknown host "${host}"`);
51
- }
52
-
53
- // Get or create conversation
54
- let conversation;
55
- let isNew = false;
39
+ let conversation = conversationId ? store.getConversation(conversationId) : null;
40
+ const resolvedHost = hostName || conversation?.host || 'local';
41
+ const resolvedSession = (tmuxSession || conversation?.tmux_session || '').trim();
56
42
 
57
- if (conversationId) {
58
- conversation = store.getConversation(conversationId);
43
+ if (!resolvedSession) {
44
+ sendSSE('error', { error: 'tmuxSession is required' });
45
+ return res.end();
46
+ }
47
+ if (!tmux.hasSession(resolvedSession, resolvedHost)) {
48
+ sendSSE('error', { error: `tmux session "${resolvedSession}" is not active` });
49
+ return res.end();
59
50
  }
60
51
 
61
52
  if (!conversation) {
62
- isNew = true;
63
- conversation = store.createConversation({
64
- engine: resolvedEngine,
65
- model: model || null,
66
- workspace: workspace || process.env.HOME,
67
- host
53
+ const existing = store.findByTmuxSession(resolvedSession);
54
+ conversation = existing || store.createConversation({
55
+ title: resolvedSession,
56
+ engine: null,
57
+ workspace: (workspace || os.homedir()).trim() || os.homedir(),
58
+ host: resolvedHost,
59
+ tmuxSession: resolvedSession,
60
+ logPath: null
68
61
  });
62
+ store.updateConversation(conversation.id, { title: resolvedSession });
69
63
  sendSSE('session_created', {
70
64
  conversationId: conversation.id,
71
65
  sessionId: conversation.id
72
66
  });
73
67
  }
74
68
 
75
- let fullMessage = message;
76
- if (Array.isArray(attachments) && attachments.length > 0) {
77
- const staged = attachments.map((file) =>
78
- attachmentManager.stageForConversation(file, conversation.id, hostObj)
79
- );
80
- const fileLines = staged.map((file) => `- ${file.originalName || file.name} (${file.promptPath})`);
81
- fullMessage = `[Attached files:\n${fileLines.join('\n')}\n]\n\n${fullMessage}`;
82
- sendSSE('status', { content: `Prepared ${staged.length} attachment(s)` });
83
- }
84
-
85
- // Context Bridge: if engine changed, add context to message
86
- if (!isNew && conversationId && conversation.engine && conversation.engine !== resolvedEngine) {
87
- try {
88
- const { default: ContextBridge } = require('../services/context-bridge');
89
- const ctx = await ContextBridge.buildContext({
90
- conversationId: conversation.id,
91
- fromEngine: conversation.engine,
92
- toEngine: resolvedEngine,
93
- userMessage: message
94
- });
95
- if (ctx.prompt) {
96
- fullMessage = ctx.prompt;
97
- console.log(`[send] Applied context bridge: ${conversation.engine} -> ${resolvedEngine} (${ctx.contextTokens} tokens)`);
98
- }
99
- } catch (err) {
100
- console.warn('[send] Context bridge failed, proceeding without context:', err.message);
101
- // Continue without context - non-fatal
102
- }
69
+ let logPath = conversation.log_path;
70
+ if (!logPath) {
71
+ logPath = tmux.ensureSessionLogging(resolvedSession, resolvedHost);
72
+ store.updateConversation(conversation.id, {
73
+ tmux_session: resolvedSession,
74
+ workspace: conversation.workspace || (workspace || os.homedir()),
75
+ log_path: logPath,
76
+ status: 'active'
77
+ });
78
+ conversation = store.getConversation(conversation.id);
103
79
  }
104
80
 
105
- // Save user message
106
81
  store.addMessage({
107
82
  conversationId: conversation.id,
108
83
  role: 'user',
109
- content: fullMessage,
110
- engine: resolvedEngine,
111
- model: model
84
+ content: message,
85
+ engine: conversation.engine,
86
+ model: conversation.model
112
87
  });
113
88
 
114
- sendSSE('message_start', { role: 'user' });
115
-
116
- // Determine tmux window name
117
- const windowName = `nc-${conversation.id.substring(0, 8)}`;
118
-
119
- // Build command for the engine
120
- const engineInfo = engines.discover()[resolvedEngine];
121
- let command;
89
+ logWatcher.watch(logPath, conversation.engine || 'generic', conversation.id);
122
90
 
123
- if (alias) {
124
- // Use a custom alias from providers.zsh
125
- command = alias;
126
- } else if (engineInfo?.available) {
127
- command = buildEngineCommand(resolvedEngine, engineInfo, {
128
- model,
129
- message: fullMessage, // Pass fullMessage for codex exec mode
130
- resume: resumeSession || (conversation.tmux_window ? conversation.id : null)
131
- });
132
- } else {
133
- sendSSE('error', { error: `Engine "${resolvedEngine}" not available` });
134
- res.end();
135
- return;
136
- }
137
-
138
- // Create or reuse tmux window
139
- let logPath;
140
- let remoteInitialSnapshot = '';
141
-
142
- if (conversation.tmux_window && tmux.hasWindow(conversation.tmux_window, hostObj)) {
143
- if (hostObj.type !== 'local') {
144
- remoteInitialSnapshot = tmux.capturePane(conversation.tmux_window, hostObj, 250);
145
- }
146
- // Resume existing window — send message directly
147
- tmux.sendKeys(conversation.tmux_window, fullMessage, { enter: true, host: hostObj });
148
- logPath = conversation.log_path;
149
- } else {
150
- // Create new tmux window
151
- const result = tmux.createWindow({
152
- windowName,
153
- command,
154
- cwd: conversation.workspace || workspace || process.env.HOME,
155
- host: hostObj
156
- });
157
- logPath = result.logPath;
158
-
159
- store.updateConversation(conversation.id, {
160
- tmux_window: windowName,
161
- log_path: logPath,
162
- status: 'active'
163
- });
164
-
165
- // Wait a moment for the CLI to start, then send the message
166
- if (resolvedEngine !== 'codex') {
167
- // Interactive CLIs need a moment to start
168
- await new Promise(r => setTimeout(r, 1500));
169
- if (hostObj.type !== 'local') {
170
- remoteInitialSnapshot = tmux.capturePane(windowName, hostObj, 250);
171
- }
172
- tmux.sendKeys(windowName, fullMessage, { enter: true, host: hostObj });
173
- } else if (hostObj.type !== 'local') {
174
- remoteInitialSnapshot = tmux.capturePane(windowName, hostObj, 250);
175
- }
176
- // Codex exec mode gets the message via the command itself
177
- }
178
-
179
- // Start watching the log
180
- if (hostObj.type === 'local' && logPath) {
181
- logWatcher.watch(logPath, resolvedEngine, conversation.id);
182
- }
183
-
184
- sendSSE('status', { content: 'Sending to tmux...' });
91
+ sendSSE('message_start', { role: 'user' });
92
+ sendSSE('status', { content: `Sending to tmux session ${resolvedSession}...` });
185
93
 
186
- // Collect response from log watcher
187
94
  let responseBuffer = '';
188
- let doneFired = false;
189
- const timeout = setTimeout(() => {
190
- if (!doneFired) {
191
- doneFired = true;
192
- finishResponse(responseBuffer || '(timeout — check tmux window directly)');
193
- }
194
- }, 120000); // 2 min timeout
195
-
196
- const onData = (data) => {
197
- if (data.conversationId !== conversation.id) return;
198
-
199
- switch (data.type) {
200
- case 'status':
201
- sendSSE('status', { content: data.content });
202
- break;
203
- case 'chunk':
204
- responseBuffer += data.content;
205
- sendSSE('chunk', { content: data.content });
206
- break;
207
- case 'raw':
208
- responseBuffer += data.content;
209
- sendSSE('chunk', { content: data.content });
210
- break;
211
- case 'message_done':
212
- if (!doneFired) {
213
- doneFired = true;
214
- clearTimeout(timeout);
215
- finishResponse(data.content || responseBuffer, data.usage);
216
- }
217
- break;
218
- case 'done':
219
- if (!doneFired) {
220
- doneFired = true;
221
- clearTimeout(timeout);
222
- finishResponse(responseBuffer || '(completed)');
223
- }
224
- break;
225
- }
226
- };
227
-
228
- const activeWatcher = hostObj.type === 'local' ? logWatcher : remotePaneWatcher;
229
- activeWatcher.on('data', onData);
230
-
231
- if (hostObj.type !== 'local') {
232
- remotePaneWatcher.watch({
233
- conversationId: conversation.id,
234
- engine: resolvedEngine,
235
- host: hostObj,
236
- windowName: conversation.tmux_window || windowName,
237
- promptText: fullMessage,
238
- initialSnapshot: remoteInitialSnapshot
239
- });
240
- }
95
+ let finished = false;
96
+ let activitySeen = false;
241
97
 
242
- function finishResponse(content, usage) {
243
- activeWatcher.off('data', onData);
244
- if (hostObj.type !== 'local') {
245
- remotePaneWatcher.unwatchByConversation(conversation.id);
246
- }
247
- clearTimeout(timeout);
98
+ const finishResponse = (content, usage) => {
99
+ if (finished) return;
100
+ finished = true;
101
+ clearTimeout(maxTimer);
102
+ clearTimeout(idleTimer);
103
+ logWatcher.off('data', onData);
248
104
 
249
- // Save assistant message
105
+ const finalContent = content || responseBuffer || tmux.capturePane(resolvedSession, resolvedHost) || '(no output captured)';
250
106
  store.addMessage({
251
107
  conversationId: conversation.id,
252
108
  role: 'assistant',
253
- content,
254
- engine: resolvedEngine,
255
- model: model,
109
+ content: finalContent,
110
+ engine: conversation.engine,
111
+ model: conversation.model,
256
112
  tokensIn: usage?.input_tokens || 0,
257
113
  tokensOut: usage?.output_tokens || 0
258
114
  });
259
115
 
260
116
  sendSSE('message_done', {
261
- content,
117
+ content: finalContent,
262
118
  usage,
263
119
  conversationId: conversation.id,
264
- engine: resolvedEngine,
265
- model: model
120
+ tmuxSession: resolvedSession
266
121
  });
267
122
 
268
123
  res.end();
269
- }
124
+ };
125
+
126
+ const bumpIdleTimer = () => {
127
+ clearTimeout(idleTimer);
128
+ idleTimer = setTimeout(() => {
129
+ finishResponse(responseBuffer);
130
+ }, activitySeen ? 15000 : 20000);
131
+ };
132
+
133
+ const onData = (data) => {
134
+ if (data.conversationId !== conversation.id) return;
135
+ activitySeen = true;
136
+
137
+ if (data.type === 'status') {
138
+ sendSSE('status', { content: data.content });
139
+ } else if (data.type === 'chunk' || data.type === 'raw') {
140
+ responseBuffer += `${data.content}\n`;
141
+ sendSSE('chunk', { content: data.content });
142
+ } else if (data.type === 'message_done') {
143
+ responseBuffer += data.content || '';
144
+ return finishResponse(data.content || responseBuffer, data.usage);
145
+ } else if (data.type === 'done') {
146
+ return finishResponse(responseBuffer);
147
+ }
148
+
149
+ bumpIdleTimer();
150
+ };
151
+
152
+ logWatcher.on('data', onData);
270
153
 
154
+ const maxTimer = setTimeout(() => {
155
+ finishResponse(responseBuffer || '(timeout — check tmux session directly)');
156
+ }, 120000);
157
+ let idleTimer = setTimeout(() => finishResponse(responseBuffer), 20000);
158
+
159
+ tmux.sendKeys(resolvedSession, message, { enter: true, host: resolvedHost });
271
160
  } catch (err) {
272
161
  console.error('[send] Error:', err);
273
162
  sendSSE('error', { error: err.message });
@@ -275,67 +164,17 @@ router.post('/', async (req, res) => {
275
164
  }
276
165
  });
277
166
 
278
- /**
279
- * POST /api/v1/send/interrupt — Interrupt current generation
280
- */
281
167
  router.post('/interrupt', (req, res) => {
282
168
  const { tmux, store } = req;
283
169
  const { conversationId } = req.body;
170
+ const conversation = conversationId ? store.getConversation(conversationId) : null;
284
171
 
285
- if (!conversationId) {
286
- return res.status(400).json({ error: 'conversationId required' });
287
- }
288
-
289
- const conversation = store.getConversation(conversationId);
290
- if (!conversation || !conversation.tmux_window) {
291
- return res.json({ success: false, reason: 'No active tmux window' });
172
+ if (!conversation?.tmux_session) {
173
+ return res.status(404).json({ error: 'Conversation has no active tmux session' });
292
174
  }
293
175
 
294
- tmux.interrupt(conversation.tmux_window, conversation.host || 'local');
295
-
296
- res.json({ success: true, method: 'tmux interrupt' });
176
+ tmux.interrupt(conversation.tmux_session, conversation.host || 'local');
177
+ res.json({ success: true });
297
178
  });
298
179
 
299
- // --- Helpers ---
300
-
301
- function resolveEngine(model, engineDiscovery) {
302
- if (!model) return 'claude';
303
- const allModels = engineDiscovery.getAllModels();
304
- const found = allModels.find(m => m.id === model);
305
- return found ? found.engine : 'claude';
306
- }
307
-
308
- function buildEngineCommand(engine, engineInfo, opts = {}) {
309
- const bin = engineInfo.path;
310
- if (!bin) return engine;
311
-
312
- switch (engine) {
313
- case 'claude':
314
- if (opts.resume) {
315
- return `${bin} --output-format json -r ${opts.resume}${opts.model ? ` --model ${opts.model}` : ''}`;
316
- }
317
- return `${bin} --output-format json${opts.model ? ` --model ${opts.model}` : ''}`;
318
-
319
- case 'codex':
320
- // For codex, message is passed via --prompt flag in exec mode
321
- const prompt = opts.message ? ` --prompt ${JSON.stringify(opts.message)}` : '';
322
- return `${bin} exec --json${opts.model ? ` -m ${opts.model}` : ''}${opts.reasoningEffort ? ` --reasoning ${opts.reasoningEffort}` : ''}${prompt}`;
323
-
324
- case 'gemini':
325
- if (opts.resume) {
326
- return `${bin} --resume ${opts.resume}${opts.model ? ` -m ${opts.model}` : ''}`;
327
- }
328
- return `${bin}${opts.model ? ` -m ${opts.model}` : ''}`;
329
-
330
- case 'qwen':
331
- if (opts.resume) {
332
- return `${bin} --resume ${opts.resume}${opts.model ? ` -m ${opts.model}` : ''}`;
333
- }
334
- return `${bin}${opts.model ? ` -m ${opts.model}` : ''}`;
335
-
336
- default:
337
- return bin;
338
- }
339
- }
340
-
341
180
  module.exports = router;
@@ -1,9 +1,8 @@
1
1
  /**
2
- * GET /api/v1/sessions — List/load sessions (with tmux_alive status)
2
+ * GET /api/v1/sessions — List/load sessions
3
3
  * GET /api/v1/sessions/:id/messages — Get session messages
4
4
  * GET /api/v1/sessions/:id/summary — Get session summary
5
5
  * POST /api/v1/sessions/:id/rename — Rename session
6
- * POST /api/v1/sessions/:id/pin — Pin/unpin session
7
6
  * DELETE /api/v1/sessions/:id — Delete session
8
7
  */
9
8
 
@@ -12,37 +11,15 @@ const router = Router();
12
11
 
13
12
  // List conversations (optionally grouped by date)
14
13
  router.get('/', (req, res) => {
15
- const { store, tmux } = req;
16
- const { workspace, groupBy, host } = req.query;
14
+ const { store } = req;
15
+ const { workspace, groupBy } = req.query;
17
16
 
18
17
  if (groupBy === 'date') {
19
- const grouped = store.listGroupedByDate({ workspace, host });
20
-
21
- // Enrich with tmux_alive status
22
- Object.keys(grouped).forEach(dateGroup => {
23
- grouped[dateGroup].forEach(conv => {
24
- if (conv.tmux_window) {
25
- conv.tmux_alive = tmux.hasWindow(conv.tmux_window, conv.host || 'local');
26
- } else {
27
- conv.tmux_alive = false;
28
- }
29
- });
30
- });
31
-
18
+ const grouped = store.listGroupedByDate({ workspace });
32
19
  return res.json(grouped);
33
20
  }
34
21
 
35
- const convs = store.listConversations({ workspace, host });
36
-
37
- // Enrich with tmux_alive status
38
- convs.forEach(conv => {
39
- if (conv.tmux_window) {
40
- conv.tmux_alive = tmux.hasWindow(conv.tmux_window, conv.host || 'local');
41
- } else {
42
- conv.tmux_alive = false;
43
- }
44
- });
45
-
22
+ const convs = store.listConversations({ workspace });
46
23
  res.json({ conversations: convs });
47
24
  });
48
25
 
@@ -56,7 +33,9 @@ router.get('/:id', (req, res) => {
56
33
  }
57
34
 
58
35
  // Check tmux window status
59
- const alive = conv.tmux_window ? tmux.hasWindow(conv.tmux_window, conv.host || 'local') : false;
36
+ const host = conv.host !== 'local' ? conv.host : null;
37
+ const target = conv.tmux_session || conv.tmux_window;
38
+ const alive = target ? tmux.hasSession(target, host) : false;
60
39
 
61
40
  res.json({
62
41
  session: {
@@ -101,30 +80,22 @@ router.post('/:id/rename', (req, res) => {
101
80
  res.json({ success: true, title });
102
81
  });
103
82
 
104
- // Pin/unpin conversation
105
- router.post('/:id/pin', (req, res) => {
106
- const { store } = req;
107
- const result = store.pinConversation(req.params.id);
108
- res.json({ pinned: result });
109
- });
110
-
111
- // Delete conversation (kills tmux window too)
83
+ // Delete conversation (kills tmux session too)
112
84
  router.delete('/:id', (req, res) => {
113
- const { store, tmux, logWatcher, remotePaneWatcher } = req;
85
+ const { store, tmux, logWatcher } = req;
114
86
  const conv = store.getConversation(req.params.id);
115
87
 
116
88
  if (!conv) return res.status(404).json({ error: 'Not found' });
117
89
 
118
- // Kill tmux window if active
119
- if (conv.tmux_window) {
120
- tmux.killWindow(conv.tmux_window, conv.host || 'local');
90
+ if (conv.tmux_session) {
91
+ const host = conv.host !== 'local' ? conv.host : null;
92
+ tmux.killSession(conv.tmux_session, host);
121
93
  }
122
94
 
123
95
  // Stop log watcher
124
96
  if (conv.log_path) {
125
97
  logWatcher.unwatch(conv.log_path);
126
98
  }
127
- remotePaneWatcher?.unwatchByConversation?.(req.params.id);
128
99
 
129
100
  store.deleteConversation(req.params.id);
130
101
  res.json({ success: true });
@@ -1,7 +1,7 @@
1
1
  /**
2
- * GET /api/v1/status — Tmux status (windows, hosts)
3
- * GET /api/v1/status/tmux — Detailed tmux info
4
- * GET /api/v1/status/pane/:window — Capture pane content
2
+ * GET /api/v1/status — tmux status
3
+ * GET /api/v1/status/tmux — Detailed tmux session info
4
+ * GET /api/v1/status/pane/:session — Capture pane content
5
5
  */
6
6
 
7
7
  const { Router } = require('express');
@@ -10,16 +10,13 @@ const router = Router();
10
10
  // Overall status
11
11
  router.get('/', (req, res) => {
12
12
  const { tmux, store } = req;
13
- const { host = 'local' } = req.query;
14
- const windows = tmux.listWindows(host);
13
+ const sessions = tmux.listSessions();
15
14
  const hosts = tmux.getAllHosts();
16
15
 
17
16
  res.json({
18
17
  tmux: {
19
- session: tmux.sessionName,
20
- alive: tmux.hasSession(host),
21
- windowCount: windows.length,
22
- windows
18
+ sessionCount: sessions.length,
19
+ sessions
23
20
  },
24
21
  hosts: hosts.map(h => ({
25
22
  ...h,
@@ -31,32 +28,31 @@ router.get('/', (req, res) => {
31
28
  // Detailed tmux windows
32
29
  router.get('/tmux', (req, res) => {
33
30
  const { tmux, store } = req;
34
- const { host = 'local' } = req.query;
35
- const windows = tmux.listWindows(host);
31
+ const sessions = tmux.listSessions();
36
32
 
37
- // Enrich with DB data
38
- const enriched = windows.map(w => {
39
- const conv = store.findByWindow(w.name);
33
+ const enriched = sessions.map((session) => {
34
+ const conv = store.findByTmuxSession(session.sessionName);
40
35
  return {
41
- ...w,
36
+ ...session,
42
37
  conversationId: conv?.id || null,
43
38
  engine: conv?.engine || null,
44
39
  model: conv?.model || null,
45
- host: conv?.host || host || 'local',
46
- title: conv?.title || w.name
40
+ host: conv?.host || 'local',
41
+ title: conv?.title || session.sessionName,
42
+ workspace: conv?.workspace || ''
47
43
  };
48
44
  });
49
45
 
50
- res.json({ windows: enriched });
46
+ res.json({ sessions: enriched });
51
47
  });
52
48
 
53
49
  // Capture pane content
54
- router.get('/pane/:window', (req, res) => {
50
+ router.get('/pane/:session', (req, res) => {
55
51
  const { tmux } = req;
56
- const { host = 'local' } = req.query;
57
- const content = tmux.capturePane(req.params.window, host);
52
+ const { host } = req.query;
53
+ const content = tmux.capturePane(req.params.session, host || null);
58
54
 
59
- res.json({ window: req.params.window, content });
55
+ res.json({ session: req.params.session, content });
60
56
  });
61
57
 
62
58
  module.exports = router;