agentp 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -11
- package/bin/tgagentp +98 -7
- package/docs/specification.md +46 -1
- package/lib/opencode.js +22 -1
- package/lib/tui-cmd.js +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,17 +230,7 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
230
230
|
|---|---|
|
|
231
231
|
| `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
|
|
232
232
|
| `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
|
|
233
|
-
| `/servers switch <name
|
|
234
|
-
| `/sessions` | List recent sessions for the current server (max 50, with date headings) |
|
|
235
|
-
| `/sessions switch <number\|name>` | Switch active session by position or partial name match |
|
|
236
|
-
| `/agents` | List primary agents (▶ marker for the active one) |
|
|
237
|
-
| `/agents switch <name>` | Switch active agent — persists on session and refreshes TUI |
|
|
238
|
-
| `/models` | List connected providers with model counts, context limits, and costs |
|
|
239
|
-
| `/status` | Show current server path, URL, busy status, and active session |
|
|
240
|
-
| `/cancel` | Cancel the current AI response for the active server |
|
|
241
|
-
| `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
|
|
242
|
-
| `/record [stop]` | Toggle recording of Telegram conversation for agentp context; `/record stop` clears and stops |
|
|
243
|
-
| `/servers switch <name> [--force]` | Switch to a server; `--force` takes over from another chat |
|
|
233
|
+
| `/servers switch <name> [--force]` | Switch active server; matches by full path, basename, or substring; `--force` takes over from another chat |
|
|
244
234
|
| `/sessions` | List recent sessions for the current server (max 50, with date headings) |
|
|
245
235
|
| `/sessions switch <number\|name>` | Switch active session by position or partial name match |
|
|
246
236
|
| `/agents` | List primary agents (▶ marker for the active one) |
|
|
@@ -256,8 +246,15 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
256
246
|
| `/allow` | Approve a permission request once |
|
|
257
247
|
| `/reject` | Deny a permission request |
|
|
258
248
|
| `/always` | Approve and remember for the session |
|
|
249
|
+
| `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
|
|
259
250
|
| `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
|
|
260
251
|
|
|
252
|
+
#### TUI Command Passthrough
|
|
253
|
+
|
|
254
|
+
Messages starting with `//` are forwarded to the OpenCode TUI as raw keystrokes (not text sent to the AI):
|
|
255
|
+
|
|
256
|
+
- **`//<command>`** — Sends `/command` directly to the TUI prompt. Useful for TUI-level commands like `/init`, `/clear`, `/history`, etc. The AI response is captured and forwarded to Telegram (or a confirmation is sent if the command is quick).
|
|
257
|
+
|
|
261
258
|
### Chat-server ownership
|
|
262
259
|
|
|
263
260
|
Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/servers switch <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
|
package/bin/tgagentp
CHANGED
|
@@ -5,7 +5,7 @@ const https = require('https');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { version } = require('../package.json');
|
|
8
|
-
const { getAuthHeaders, sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, sendToSessionAsync, respondToPermission, listenForSessionEvents, listAgents, listProviders, isServerAlive } = require('../lib/opencode.js');
|
|
8
|
+
const { getAuthHeaders, sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, sendToSessionAsync, respondToPermission, respondToQuestion, listenForSessionEvents, listAgents, listProviders, isServerAlive } = require('../lib/opencode.js');
|
|
9
9
|
|
|
10
10
|
// Logger: info→stdout, error→stderr (always), debug→stderr (--verbose only)
|
|
11
11
|
const log = {
|
|
@@ -19,7 +19,8 @@ const TGAGENTP_PORT = parseInt(process.env.TGAGENTP_PORT || '0', 10);
|
|
|
19
19
|
const PORT_FILE = '/tmp/tgagentp-port';
|
|
20
20
|
const TGAGENTP_DEBOUNCE_MS = parseInt(process.env.TGAGENTP_DEBOUNCE_MS || '5000', 10);
|
|
21
21
|
const CONNECTIONS_FILE = '/tmp/tgagentp-connections.json';
|
|
22
|
-
const { listServers, activateServer, resurrectServer, statefileFor, readState } = require('../lib/ocmux.js');
|
|
22
|
+
const { listServers, activateServer, resurrectServer, statefileFor, readState, tuiPaneId } = require('../lib/ocmux.js');
|
|
23
|
+
const { executeTuiCommand } = require('../lib/tui-cmd.js');
|
|
23
24
|
|
|
24
25
|
// Persist chat↔server directory mappings across restarts
|
|
25
26
|
function saveConnection(chatId, threadId, dir) {
|
|
@@ -383,14 +384,17 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
383
384
|
'with status, context limit, and input/output costs.');
|
|
384
385
|
return;
|
|
385
386
|
}
|
|
386
|
-
if (topic === 'allow' || topic === 'reject' || topic === 'always') {
|
|
387
|
+
if (topic === 'allow' || topic === 'reject' || topic === 'always' || topic === 'answer') {
|
|
387
388
|
await sendLongMessage(token, chatState,
|
|
388
389
|
'`/allow` ─ approve a permission request once\n' +
|
|
389
390
|
'`/reject` ─ deny a permission request\n' +
|
|
390
|
-
'`/always` ─ approve and remember for the session\n
|
|
391
|
-
'
|
|
391
|
+
'`/always` ─ approve and remember for the session\n' +
|
|
392
|
+
'`/answer <number>` ─ respond to a question from the AI\n\n' +
|
|
393
|
+
'These commands respond to OpenCode prompts forwarded by tgagentp.\n' +
|
|
392
394
|
'When OpenCode needs approval (e.g. to run a shell command), tgagentp sends a\n' +
|
|
393
|
-
'🔐 notification to the chat. Use one of these commands to answer
|
|
395
|
+
'🔐 notification to the chat. Use one of these commands to answer.\n' +
|
|
396
|
+
'When the AI asks a multiple-choice question, tgagentp sends a ❓ notification\n' +
|
|
397
|
+
'with numbered options. Use `/answer <number>` to respond.');
|
|
394
398
|
return;
|
|
395
399
|
}
|
|
396
400
|
if (topic === 'think') {
|
|
@@ -1061,6 +1065,24 @@ async function cmdAlways(token, chatId, args, chatState, setServerForChat, setAc
|
|
|
1061
1065
|
}
|
|
1062
1066
|
}
|
|
1063
1067
|
|
|
1068
|
+
async function cmdAnswer(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1069
|
+
const st = getState(chatState.serverBase);
|
|
1070
|
+
if (!st.pendingQuestion) {
|
|
1071
|
+
await sendLongMessage(token, chatState, 'No pending question. Use `/status` to check the current state.');
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const q = st.pendingQuestion;
|
|
1075
|
+
const n = parseInt(args[0], 10);
|
|
1076
|
+
if (isNaN(n) || n < 1 || n > q.options.length) {
|
|
1077
|
+
await sendLongMessage(token, chatState, `Invalid answer. Use a number between 1 and ${q.options.length}.`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const answer = q.options[n - 1].text || q.options[n - 1];
|
|
1081
|
+
await respondToQuestion(q.server, q.sessionId, q.questionId, answer);
|
|
1082
|
+
await sendLongMessage(token, chatState, `✅ Answered: ${answer}`);
|
|
1083
|
+
st.pendingQuestion = null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1064
1086
|
async function cmdThink(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1065
1087
|
const st = getState(chatState.serverBase);
|
|
1066
1088
|
const sub = args && args.length > 0 ? args[0].toLowerCase() : 'switch';
|
|
@@ -1331,6 +1353,7 @@ const COMMANDS = {
|
|
|
1331
1353
|
'allow': cmdAllow,
|
|
1332
1354
|
'reject': cmdReject,
|
|
1333
1355
|
'always': cmdAlways,
|
|
1356
|
+
'answer': cmdAnswer,
|
|
1334
1357
|
};
|
|
1335
1358
|
|
|
1336
1359
|
// --------------------------------------------------------------------------
|
|
@@ -1339,7 +1362,7 @@ const COMMANDS = {
|
|
|
1339
1362
|
|
|
1340
1363
|
const serverState = {};
|
|
1341
1364
|
function getState(url) {
|
|
1342
|
-
return serverState[url] = serverState[url] || { busy: false, serverDead: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, thinkingEnabled: false, messageQueue: [] };
|
|
1365
|
+
return serverState[url] = serverState[url] || { busy: false, serverDead: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, pendingQuestion: null, thinkingEnabled: false, messageQueue: [] };
|
|
1343
1366
|
}
|
|
1344
1367
|
|
|
1345
1368
|
function sessionLabel(st) {
|
|
@@ -1614,6 +1637,21 @@ async function main() {
|
|
|
1614
1637
|
log.debug(` → permission_replied: clearing pendingPermission`);
|
|
1615
1638
|
getState(url).pendingPermission = null;
|
|
1616
1639
|
},
|
|
1640
|
+
onQuestion: (q) => {
|
|
1641
|
+
const st2 = getState(url);
|
|
1642
|
+
const qDump = JSON.stringify(q);
|
|
1643
|
+
log.debug(` → question_asked callback: ${qDump}`);
|
|
1644
|
+
st2.pendingQuestion = { server: url, sessionId, questionId: q.id, question: q.question || 'Unknown question', options: q.options || [] };
|
|
1645
|
+
const options = (q.options || []).map((opt, i) => `${i + 1}. ${opt.text || opt}`).join('\n');
|
|
1646
|
+
const msg = '❓ **Question from the AI**\n\n'
|
|
1647
|
+
+ (q.question || 'Choose an option:') + '\n\n'
|
|
1648
|
+
+ (options ? options + '\n\n' : '')
|
|
1649
|
+
+ 'Use `/answer <number>` to respond.';
|
|
1650
|
+
log.debug(` → sending question notification to chat ${chatId}`);
|
|
1651
|
+
sendLongMessage(token, chatState, msg, replyTo, threadId)
|
|
1652
|
+
.then(() => log.debug(` → question notification sent OK`))
|
|
1653
|
+
.catch((err) => log.debug(` → question notification failed: ${err.message}`));
|
|
1654
|
+
},
|
|
1617
1655
|
}, cancelRef).catch(() => {});
|
|
1618
1656
|
await onConnected;
|
|
1619
1657
|
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
|
|
@@ -1799,6 +1837,12 @@ async function main() {
|
|
|
1799
1837
|
await sendLongMessage(token, chatState, '⏳ Server is busy. Use `/cancel` to cancel or note again later.');
|
|
1800
1838
|
return;
|
|
1801
1839
|
}
|
|
1840
|
+
|
|
1841
|
+
// Auto-switch tmux window to this chat's server
|
|
1842
|
+
const srvList = listServers();
|
|
1843
|
+
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(url));
|
|
1844
|
+
if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
1845
|
+
|
|
1802
1846
|
const intro = 'The following information is for your awareness. Reply with only "Ack" and do not take any action:';
|
|
1803
1847
|
st.busy = true;
|
|
1804
1848
|
const myTaskId = ++st.taskId;
|
|
@@ -1989,6 +2033,47 @@ async function main() {
|
|
|
1989
2033
|
// Build reply context if this message is a Telegram reply
|
|
1990
2034
|
const replyContext = quoteReply(msg);
|
|
1991
2035
|
|
|
2036
|
+
// Intercept //commands (raw TUI passthrough via tmux send-keys)
|
|
2037
|
+
if (text.startsWith('//')) {
|
|
2038
|
+
const tuiCmd = text.slice(1);
|
|
2039
|
+
if (!cs.serverBase) {
|
|
2040
|
+
await sendLongMessage(token, cs, '🔌 Not connected to any server.');
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
const st = getState(cs.serverBase);
|
|
2044
|
+
if (st.busy) {
|
|
2045
|
+
await sendLongMessage(token, cs, '⏳ Server is busy. Use `/cancel` to cancel first.');
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
const srvList = listServers();
|
|
2049
|
+
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(cs.serverBase));
|
|
2050
|
+
if (!srvMatch) {
|
|
2051
|
+
await sendLongMessage(token, cs, '❌ Server not found in tmux.');
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
2055
|
+
const paneId = tuiPaneId(srvMatch.index);
|
|
2056
|
+
if (!paneId) {
|
|
2057
|
+
await sendLongMessage(token, cs, '❌ TUI pane not found.');
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
st.busy = true;
|
|
2061
|
+
try {
|
|
2062
|
+
const answer = await executeTuiCommand(paneId, cs.serverBase, tuiCmd);
|
|
2063
|
+
if (answer && answer.trim()) {
|
|
2064
|
+
await sendLongMessage(token, cs, answer.trim());
|
|
2065
|
+
} else {
|
|
2066
|
+
await sendLongMessage(token, cs, `✅ \`${tuiCmd}\` submitted.`);
|
|
2067
|
+
}
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
log.error(`Error executing TUI command ${tuiCmd}: ${err.message}`);
|
|
2070
|
+
try { await sendLongMessage(token, cs, `Error executing \`${tuiCmd}\`: ${err.message}`); } catch (_) {}
|
|
2071
|
+
} finally {
|
|
2072
|
+
st.busy = false;
|
|
2073
|
+
}
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1992
2077
|
// Intercept /commands
|
|
1993
2078
|
if (text.startsWith('/')) {
|
|
1994
2079
|
const parts = text.slice(1).split(' ');
|
|
@@ -2061,6 +2146,12 @@ async function main() {
|
|
|
2061
2146
|
await sendLongMessage(token, cs, '⏳ Server is busy. Use `/cancel` to cancel or `/queue <message>` to queue your message.');
|
|
2062
2147
|
continue;
|
|
2063
2148
|
}
|
|
2149
|
+
|
|
2150
|
+
// Auto-switch tmux window to this chat's server
|
|
2151
|
+
const srvList = listServers();
|
|
2152
|
+
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(cs.serverBase));
|
|
2153
|
+
if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
2154
|
+
|
|
2064
2155
|
st.busy = true;
|
|
2065
2156
|
const myTaskId = ++st.taskId;
|
|
2066
2157
|
log.debug(` → started background processing (task ${myTaskId})`);
|
package/docs/specification.md
CHANGED
|
@@ -16,7 +16,7 @@ All tools share `lib/opencode.js` (HTTP session API client) and `lib/ocmux.js` (
|
|
|
16
16
|
|
|
17
17
|
### `package.json`
|
|
18
18
|
|
|
19
|
-
**Version:** 0.
|
|
19
|
+
**Version:** 0.11.2
|
|
20
20
|
|
|
21
21
|
Fields:
|
|
22
22
|
- `"bin"` — registers `agentp`, `ocmux`, `tgagentp`
|
|
@@ -117,6 +117,7 @@ Shared by `bin/agentp` and `bin/tgagentp`. Functions:
|
|
|
117
117
|
| `sendToSession(server, id, text, agent?, cancelRef?)` | POST /session/:id/message | Synchronous message; optional abort |
|
|
118
118
|
| `sendToSessionAsync(server, id, text, agent?)` | POST /session/:id/prompt_async | Non-blocking (204); answer via SSE |
|
|
119
119
|
| `respondToPermission(server, id, permissionId, response)` | POST /session/:id/permissions/:id | Permission response |
|
|
120
|
+
| `respondToQuestion(server, id, questionId, answer)` | POST /session/:id/questions/:id | Question response |
|
|
120
121
|
| `selectSession(server, id)` | POST /session/:id/select | TUI navigation |
|
|
121
122
|
| `listAgents(server)` | GET /agent | Returns parsed array |
|
|
122
123
|
| `listProviders(server)` | GET /provider | Returns parsed array |
|
|
@@ -342,6 +343,50 @@ Both command and non-command paths in the message loop check `cs.serverBase`:
|
|
|
342
343
|
|
|
343
344
|
---
|
|
344
345
|
|
|
346
|
+
## `//command` TUI Passthrough
|
|
347
|
+
|
|
348
|
+
Messages starting with `//` are forwarded to the TUI as raw keystrokes (not AI text). The `//` prefix is stripped, a trailing space is appended to activate the TUI as-you-type menu, and `tmux send-keys` sends the keys to the TUI pane. An SSE listener connects before the Enter key to catch the AI response.
|
|
349
|
+
|
|
350
|
+
**Flow:**
|
|
351
|
+
1. Chat message: `//init`
|
|
352
|
+
2. Strips `//` → `init`
|
|
353
|
+
3. Prepends `/` → `/init`
|
|
354
|
+
4. Appends space → `/init ` (selects TUI menu item)
|
|
355
|
+
5. `tmux send-keys` to TUI pane: `C-u` (clear line), type `/init `, Enter
|
|
356
|
+
6. SSE listener starts before Enter (15s timeout)
|
|
357
|
+
7. If AI responds: forwards full answer to Telegram
|
|
358
|
+
8. If timeout: sends `✅ /init submitted.` confirmation
|
|
359
|
+
|
|
360
|
+
**Busy guard:** `//command` is blocked when `st.busy` is true (same "use /cancel" message as regular messages).
|
|
361
|
+
|
|
362
|
+
**Commands that trigger AI responses:** `//init`, `//clear`, `//history` (and any other TUI command that produces an AI answer). Quick commands like `/exit` timeout without an answer.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Question Handling
|
|
367
|
+
|
|
368
|
+
When the AI asks a structured multiple-choice question (via `question.asked` SSE event), tgagentp forwards the question to Telegram with numbered options.
|
|
369
|
+
|
|
370
|
+
**Notification format:**
|
|
371
|
+
```
|
|
372
|
+
❓ **Question from the AI**
|
|
373
|
+
|
|
374
|
+
What would you like to test?
|
|
375
|
+
|
|
376
|
+
1. Option A
|
|
377
|
+
2. Option B
|
|
378
|
+
3. Option C
|
|
379
|
+
|
|
380
|
+
Use /answer <number> to respond.
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Response:**
|
|
384
|
+
- `/answer <number>` → `POST /session/:id/questions/:id` with the selected option's value
|
|
385
|
+
- Only one question can be pending at a time per server
|
|
386
|
+
- The question is stored per-server in `chatStates.pendingQuestion`
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
345
390
|
## Logging Conventions (tgagentp)
|
|
346
391
|
|
|
347
392
|
- **stdout** — informational messages (startup, discovery, session switches)
|
package/lib/opencode.js
CHANGED
|
@@ -318,7 +318,7 @@ async function respondToPermission(server, sessionId, permissionId, response, re
|
|
|
318
318
|
|
|
319
319
|
// Listen for session events via SSE, collecting assistant text parts and detecting
|
|
320
320
|
// permission requests. Resolves with the full collected text when the session goes idle.
|
|
321
|
-
// callbacks: { onText(chunk), onPermission(permission), onThinking(chunk), onConnected() }
|
|
321
|
+
// callbacks: { onText(chunk), onPermission(permission), onThinking(chunk), onConnected(), onQuestion(question) }
|
|
322
322
|
// cancelRef — allows aborting the SSE stream via req.destroy()
|
|
323
323
|
// logFn — optional logger; if provided, used instead of process.stderr.write
|
|
324
324
|
function listenForSessionEvents(server, sessionId, callbacks, cancelRef, logFn) {
|
|
@@ -413,6 +413,18 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef, logFn)
|
|
|
413
413
|
continue;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
if (event.type === 'question.asked') {
|
|
417
|
+
const qStr = JSON.stringify(props);
|
|
418
|
+
sseLog(` [SSE] QUESTION ASKED: ${qStr}\n`);
|
|
419
|
+
if (callbacks.onQuestion) {
|
|
420
|
+
sseLog(` [SSE] calling onQuestion callback\n`);
|
|
421
|
+
callbacks.onQuestion(props);
|
|
422
|
+
} else {
|
|
423
|
+
sseLog(` [SSE] WARNING: no onQuestion callback registered\n`);
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
416
428
|
// Completion signals
|
|
417
429
|
if (event.type === 'session.idle') {
|
|
418
430
|
clearTimeout(safetyTimer);
|
|
@@ -510,6 +522,14 @@ function isServerAlive(url) {
|
|
|
510
522
|
});
|
|
511
523
|
}
|
|
512
524
|
|
|
525
|
+
// Respond to a question (choice) asked by the AI.
|
|
526
|
+
async function respondToQuestion(server, sessionId, questionId, answer) {
|
|
527
|
+
const url = `${server}/session/${encodeURIComponent(sessionId)}/questions/${encodeURIComponent(questionId)}`;
|
|
528
|
+
const body = JSON.stringify({ answer });
|
|
529
|
+
const { status } = await makeRequest(buildJsonRequest(url, 'POST', body), body);
|
|
530
|
+
if (status !== 200) throw new Error(`Failed to respond to question: ${status}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
513
533
|
module.exports = {
|
|
514
534
|
getAuthHeaders,
|
|
515
535
|
makeRequest,
|
|
@@ -519,6 +539,7 @@ module.exports = {
|
|
|
519
539
|
submitPrompt,
|
|
520
540
|
sendText,
|
|
521
541
|
listenForFinalAnswer,
|
|
542
|
+
respondToQuestion,
|
|
522
543
|
listSessions,
|
|
523
544
|
getSession,
|
|
524
545
|
createSession,
|
package/lib/tui-cmd.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const child_process = require('child_process')
|
|
2
|
+
const { listenForFinalAnswer } = require('./opencode.js')
|
|
3
|
+
|
|
4
|
+
async function executeTuiCommand(paneId, server, command) {
|
|
5
|
+
const commandWithSpace = command + ' '
|
|
6
|
+
const cancelRef = { current: null }
|
|
7
|
+
|
|
8
|
+
const answerPromise = listenForFinalAnswer(server, null, cancelRef)
|
|
9
|
+
|
|
10
|
+
child_process.spawnSync('tmux', ['send-keys', '-t', paneId, 'C-u'])
|
|
11
|
+
child_process.spawnSync('tmux', ['send-keys', '-t', paneId, '-l', commandWithSpace])
|
|
12
|
+
child_process.spawnSync('tmux', ['send-keys', '-t', paneId, 'Enter'])
|
|
13
|
+
|
|
14
|
+
const timeoutMs = 15000
|
|
15
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
16
|
+
setTimeout(() => reject(new Error('timeout')), timeoutMs)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return await Promise.race([answerPromise, timeoutPromise])
|
|
21
|
+
} catch {
|
|
22
|
+
if (cancelRef.current) {
|
|
23
|
+
cancelRef.current.destroy()
|
|
24
|
+
cancelRef.current = null
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { executeTuiCommand }
|