pi-pier 0.2.4 → 0.2.6
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 +37 -8
- package/package.json +1 -1
- package/skills/pi-herdr-workflow/SKILL.md +96 -15
- package/src/core/subagent.ts +246 -332
- package/src/core/terminal.ts +200 -232
- package/src/core/todo.ts +78 -79
- package/src/index-master.ts +4 -2
- package/src/index-runtime.ts +6 -2
- package/src/index-worker.ts +11 -5
- package/src/index.ts +72 -31
- package/src/notice-buffer.ts +2 -2
- package/src/roles/master.json +4 -4
- package/src/roles/worker-default.json +3 -12
- package/src/slim-frame.ts +120 -11
- package/src/subagent-core.ts +3 -3
- package/src/subagent-poll-loop.ts +1 -1
- package/src/terminal-core.ts +3 -3
- package/src/todo-window.ts +82 -0
package/src/core/terminal.ts
CHANGED
|
@@ -99,258 +99,226 @@ export default function terminalPlugin(ctx: Context): void {
|
|
|
99
99
|
const errText = (msg: string) => ({ content: [{ type: 'text' as const, text: `Error: ${msg}` }], details: {} });
|
|
100
100
|
|
|
101
101
|
scoped.registerTool({
|
|
102
|
-
name: '
|
|
103
|
-
label: 'Terminal
|
|
102
|
+
name: 'terminal',
|
|
103
|
+
label: 'Terminal',
|
|
104
104
|
description: [
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'A human can also type into the pane directly; terminal_close closes it.',
|
|
105
|
+
'Manage persistent interactive terminals (resident shells in dedicated herdr panes).',
|
|
106
|
+
'Operations: open (create session), send (type commands), read (capture output), signal (ctrl+c/ctrl+d/ctrl+z/esc/enter), close (kill shell), list (show all).',
|
|
107
|
+
'The shell keeps cwd, environment variables, and background processes across calls.',
|
|
108
|
+
'Use for dev servers, REPLs, or multi-step shell work — not one-shot bash calls.',
|
|
110
109
|
].join(' '),
|
|
111
110
|
parameters: Type.Object({
|
|
112
|
-
|
|
111
|
+
action: Type.Union([
|
|
112
|
+
Type.Literal('open'),
|
|
113
|
+
Type.Literal('send'),
|
|
114
|
+
Type.Literal('read'),
|
|
115
|
+
Type.Literal('signal'),
|
|
116
|
+
Type.Literal('close'),
|
|
117
|
+
Type.Literal('list'),
|
|
118
|
+
], { description: 'Terminal operation to perform' }),
|
|
119
|
+
cwd: Type.Optional(Type.String({ description: '[open] Working directory for the shell (defaults to session cwd)' })),
|
|
120
|
+
terminal_id: Type.Optional(Type.String({ description: '[send|read|signal|close] Terminal id returned by open action' })),
|
|
121
|
+
text: Type.Optional(Type.String({ description: '[send] Command text to type and run (Enter appended automatically)' })),
|
|
122
|
+
pane_id: Type.Optional(Type.String({ description: '[read] Direct pane read (limited to own tab panes only)' })),
|
|
123
|
+
max_chars: Type.Optional(Type.Number({ description: '[read] Output cap for this read (default 10000)' })),
|
|
124
|
+
key: Type.Optional(Type.String({ description: '[signal] Control key: ctrl+c | ctrl+d | ctrl+z | esc | enter' })),
|
|
113
125
|
}),
|
|
114
126
|
async execute(_tc, params, _sig, _upd, ctx) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
: (ctx
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
paneId = await client.splitPane({ direction: 'right', cwd, focus: false, targetPaneId: env.paneId });
|
|
128
|
-
} catch (e) {
|
|
129
|
-
return errText(`failed to split terminal pane: ${(e as Error).message}`);
|
|
130
|
-
}
|
|
131
|
-
r.entry.paneId = paneId;
|
|
132
|
-
terminals = r.entries;
|
|
133
|
-
persistTerminals();
|
|
134
|
-
const prompt = promptStrategyFor();
|
|
135
|
-
let readiness: 'prompt' | 'silent' | 'busy' = 'busy';
|
|
136
|
-
try {
|
|
137
|
-
const matched = await client.waitForOutput(paneId, { type: 'regex', value: prompt.waitPattern }, READINESS_TIMEOUT_MS);
|
|
138
|
-
if (matched) readiness = 'prompt';
|
|
139
|
-
else {
|
|
140
|
-
const read = await client.readPane(paneId, { stripAnsi: false });
|
|
141
|
-
readiness = classifyReadiness(stripAnsi(read.text), { silentMs: 0, prompt });
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
/* Readiness is advisory, so probe failures still leave a usable terminal. */
|
|
127
|
+
const action = typeof params?.action === 'string' ? params.action : '';
|
|
128
|
+
switch (action) {
|
|
129
|
+
case 'open': return executeTerminalOpen(params, ctx);
|
|
130
|
+
case 'send': return executeTerminalSend(params);
|
|
131
|
+
case 'read': return executeTerminalRead(params);
|
|
132
|
+
case 'signal': return executeTerminalSignal(params);
|
|
133
|
+
case 'close': return executeTerminalClose(params);
|
|
134
|
+
case 'list': return executeTerminalList();
|
|
135
|
+
default:
|
|
136
|
+
return errText(action
|
|
137
|
+
? `unknown action "${action}" (valid: open, send, read, signal, close, list)`
|
|
138
|
+
: 'action is required (open, send, read, signal, close, list)');
|
|
145
139
|
}
|
|
146
|
-
const text = [
|
|
147
|
-
`terminal ${r.entry.terminalId} open (pane ${paneId})`,
|
|
148
|
-
`cwd: ${cwd}`,
|
|
149
|
-
`readiness: ${readiness}${readiness !== 'prompt' ? ' (prompt not detected yet — check with terminal_read before sending)' : ''}`,
|
|
150
|
-
].join('\n');
|
|
151
|
-
return { content: [{ type: 'text', text }], details: { terminal_id: r.entry.terminalId, pane_id: paneId, readiness } };
|
|
152
140
|
},
|
|
153
141
|
});
|
|
154
142
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
143
|
+
async function executeTerminalOpen(params, ctx) {
|
|
144
|
+
if (!client.available || !env) return errText('terminal tools require a herdr-managed pane');
|
|
145
|
+
const cwd = typeof params?.cwd === 'string' && params.cwd ? params.cwd
|
|
146
|
+
: (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
147
|
+
const r = registerTerminal(terminals, {
|
|
148
|
+
paneId: env.paneId, // Reserve a valid id until split returns the terminal pane.
|
|
149
|
+
tabId: env.tabId,
|
|
150
|
+
cwd,
|
|
151
|
+
createdAt: Date.now(),
|
|
152
|
+
});
|
|
153
|
+
if (!r.ok) return errText(r.error);
|
|
154
|
+
let paneId: string;
|
|
155
|
+
try {
|
|
156
|
+
paneId = await client.splitPane({ direction: 'right', cwd, focus: false, targetPaneId: env.paneId });
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return errText(`failed to split terminal pane: ${(e as Error).message}`);
|
|
159
|
+
}
|
|
160
|
+
r.entry.paneId = paneId;
|
|
161
|
+
terminals = r.entries;
|
|
162
|
+
persistTerminals();
|
|
163
|
+
const prompt = promptStrategyFor();
|
|
164
|
+
let readiness: 'prompt' | 'silent' | 'busy' = 'busy';
|
|
165
|
+
try {
|
|
166
|
+
const matched = await client.waitForOutput(paneId, { type: 'regex', value: prompt.waitPattern }, READINESS_TIMEOUT_MS);
|
|
167
|
+
if (matched) readiness = 'prompt';
|
|
168
|
+
else {
|
|
169
|
+
const read = await client.readPane(paneId, { stripAnsi: false });
|
|
170
|
+
readiness = classifyReadiness(stripAnsi(read.text), { silentMs: 0, prompt });
|
|
176
171
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
172
|
+
} catch {
|
|
173
|
+
/* Readiness is advisory, so probe failures still leave a usable terminal. */
|
|
174
|
+
}
|
|
175
|
+
const text = [
|
|
176
|
+
`terminal ${r.entry.terminalId} open (pane ${paneId})`,
|
|
177
|
+
`cwd: ${cwd}`,
|
|
178
|
+
`readiness: ${readiness}${readiness !== 'prompt' ? ' (prompt not detected yet — check with action read before sending)' : ''}`,
|
|
179
|
+
].join('\n');
|
|
180
|
+
return { content: [{ type: 'text', text }], details: { terminal_id: r.entry.terminalId, pane_id: paneId, readiness } };
|
|
181
|
+
}
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}),
|
|
196
|
-
async execute(_tc, params) {
|
|
197
|
-
let paneId: string | null = null;
|
|
198
|
-
let entry: TerminalEntry | null = null;
|
|
199
|
-
if (typeof params?.terminal_id === 'string') {
|
|
200
|
-
entry = findOpenTerminal(params.terminal_id);
|
|
201
|
-
if (!entry) return errText(`unknown or closed terminal "${params.terminal_id}" (see terminal_list)`);
|
|
202
|
-
paneId = entry.paneId;
|
|
203
|
-
} else if (typeof params?.pane_id === 'string') {
|
|
204
|
-
// T5 limits direct reads to this task tab or owned terminals to prevent cross-task access.
|
|
205
|
-
try {
|
|
206
|
-
const panes = await client.listPanes();
|
|
207
|
-
const target = panes.find((p) => p.paneId === params.pane_id);
|
|
208
|
-
const ownTab = target?.tabId && target.tabId === env?.tabId;
|
|
209
|
-
const ownTerminal = activeTerminalPaneIds(terminals).has(params.pane_id);
|
|
210
|
-
if (!target || (!ownTab && !ownTerminal)) {
|
|
211
|
-
return errText('pane_id read is limited to panes in this session\'s own tab or self-created terminal panes');
|
|
212
|
-
}
|
|
213
|
-
paneId = params.pane_id;
|
|
214
|
-
} catch (e) {
|
|
215
|
-
return errText(`pane lookup failed: ${(e as Error).message}`);
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
return errText('provide terminal_id (or pane_id for a direct own-tab read)');
|
|
219
|
-
}
|
|
220
|
-
const maxChars = typeof params?.max_chars === 'number' && params.max_chars > 0
|
|
221
|
-
? Math.min(params.max_chars, TERM_READ_MAX) : TERM_READ_MAX;
|
|
222
|
-
let read: { text: string; revision: number; truncated: boolean };
|
|
223
|
-
try {
|
|
224
|
-
read = await client.readPane(paneId, { stripAnsi: false });
|
|
225
|
-
} catch (e) {
|
|
226
|
-
return errText(`read failed (pane may be closed): ${(e as Error).message}`);
|
|
227
|
-
}
|
|
228
|
-
// T6 inspects raw output because ANSI stripping would erase alternate-screen evidence.
|
|
229
|
-
const tui = detectFullscreenTUI(read.text);
|
|
230
|
-
if (tui.detected) {
|
|
231
|
-
const head = stripAnsi(read.text).slice(0, 200);
|
|
232
|
-
const text = [
|
|
233
|
-
'FULLSCREEN_TUI_DETECTED: this terminal is running a fullscreen program (e.g. vim/less/top); its screen output is not readable here.',
|
|
234
|
-
'Suggestions:',
|
|
235
|
-
'1. Run such programs in a native herdr pane where the human can interact directly.',
|
|
236
|
-
'2. Use non-interactive alternatives (cat/grep instead of less; edit/write tools instead of vim).',
|
|
237
|
-
`Raw head (first 200 chars): ${head}`,
|
|
238
|
-
].join('\n');
|
|
239
|
-
return { content: [{ type: 'text', text }], details: { error: 'FULLSCREEN_TUI_DETECTED' } };
|
|
240
|
-
}
|
|
241
|
-
const prev: ReadCursor | null = entry
|
|
242
|
-
? (entry.readRevision != null
|
|
243
|
-
? { revision: entry.readRevision, len: entry.readLen, tail: entry.readTail, eoTail: entry.readEoTail }
|
|
244
|
-
: null)
|
|
245
|
-
: null;
|
|
246
|
-
const inc = computeIncrement(prev, { text: read.text, revision: read.revision }, maxChars);
|
|
247
|
-
if (entry) {
|
|
248
|
-
entry.readRevision = inc.cursor.revision;
|
|
249
|
-
entry.readLen = inc.cursor.len;
|
|
250
|
-
entry.readTail = inc.cursor.tail;
|
|
251
|
-
entry.readEoTail = inc.cursor.eoTail;
|
|
252
|
-
touchTerminal(entry);
|
|
253
|
-
}
|
|
254
|
-
if (inc.mode === 'none') {
|
|
255
|
-
return { content: [{ type: 'text', text: 'no new output' }], details: { mode: 'none', revision: read.revision } };
|
|
256
|
-
}
|
|
257
|
-
const output = stripAnsi(inc.text);
|
|
258
|
-
const text = output.length ? output : '(empty)';
|
|
259
|
-
return {
|
|
260
|
-
content: [{ type: 'text', text }],
|
|
261
|
-
details: { mode: inc.mode, revision: read.revision, hard_capped: inc.hardCapped, source_revision_truncated: read.truncated },
|
|
262
|
-
};
|
|
263
|
-
},
|
|
264
|
-
});
|
|
183
|
+
async function executeTerminalSend(params) {
|
|
184
|
+
const entry = findOpenTerminal(params?.terminal_id);
|
|
185
|
+
if (!entry) return errText(`unknown or closed terminal "${String(params?.terminal_id)}" (see action list)`);
|
|
186
|
+
const v = validateSendText(typeof params?.text === 'string' ? params.text : '');
|
|
187
|
+
if (!v.ok) return errText(v.error);
|
|
188
|
+
try {
|
|
189
|
+
await client.sendPaneText(entry.paneId, v.text);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return errText(`send failed (pane may be closed): ${(e as Error).message}`);
|
|
192
|
+
}
|
|
193
|
+
touchTerminal(entry);
|
|
194
|
+
return { content: [{ type: 'text', text: `sent to ${entry.terminalId} (${v.text.length} chars)` }], details: { terminal_id: entry.terminalId } };
|
|
195
|
+
}
|
|
265
196
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
key: Type.String({ description: 'One of: ctrl+c, ctrl+d, ctrl+z, esc, enter' }),
|
|
276
|
-
}),
|
|
277
|
-
async execute(_tc, params) {
|
|
278
|
-
const entry = findOpenTerminal(params?.terminal_id);
|
|
279
|
-
if (!entry) return errText(`unknown or closed terminal "${String(params?.terminal_id)}" (see terminal_list)`);
|
|
280
|
-
const v = validateSignal(typeof params?.key === 'string' ? params.key : '');
|
|
281
|
-
if (!v.ok) return errText(v.error);
|
|
197
|
+
async function executeTerminalRead(params) {
|
|
198
|
+
let paneId: string | null = null;
|
|
199
|
+
let entry: TerminalEntry | null = null;
|
|
200
|
+
if (typeof params?.terminal_id === 'string') {
|
|
201
|
+
entry = findOpenTerminal(params.terminal_id);
|
|
202
|
+
if (!entry) return errText(`unknown or closed terminal "${params.terminal_id}" (see action list)`);
|
|
203
|
+
paneId = entry.paneId;
|
|
204
|
+
} else if (typeof params?.pane_id === 'string') {
|
|
205
|
+
// T5 limits direct reads to this task tab or owned terminals to prevent cross-task access.
|
|
282
206
|
try {
|
|
283
|
-
await client.
|
|
207
|
+
const panes = await client.listPanes();
|
|
208
|
+
const target = panes.find((p) => p.paneId === params.pane_id);
|
|
209
|
+
const ownTab = target?.tabId && target.tabId === env?.tabId;
|
|
210
|
+
const ownTerminal = activeTerminalPaneIds(terminals).has(params.pane_id);
|
|
211
|
+
if (!target || (!ownTab && !ownTerminal)) {
|
|
212
|
+
return errText('pane_id read is limited to panes in this session\'s own tab or self-created terminal panes');
|
|
213
|
+
}
|
|
214
|
+
paneId = params.pane_id;
|
|
284
215
|
} catch (e) {
|
|
285
|
-
return errText(`
|
|
216
|
+
return errText(`pane lookup failed: ${(e as Error).message}`);
|
|
286
217
|
}
|
|
218
|
+
} else {
|
|
219
|
+
return errText('provide terminal_id (or pane_id for a direct own-tab read)');
|
|
220
|
+
}
|
|
221
|
+
const maxChars = typeof params?.max_chars === 'number' && params.max_chars > 0
|
|
222
|
+
? Math.min(params.max_chars, TERM_READ_MAX) : TERM_READ_MAX;
|
|
223
|
+
let read: { text: string; revision: number; truncated: boolean };
|
|
224
|
+
try {
|
|
225
|
+
read = await client.readPane(paneId, { stripAnsi: false });
|
|
226
|
+
} catch (e) {
|
|
227
|
+
return errText(`read failed (pane may be closed): ${(e as Error).message}`);
|
|
228
|
+
}
|
|
229
|
+
// T6 inspects raw output because ANSI stripping would erase alternate-screen evidence.
|
|
230
|
+
const tui = detectFullscreenTUI(read.text);
|
|
231
|
+
if (tui.detected) {
|
|
232
|
+
const head = stripAnsi(read.text).slice(0, 200);
|
|
233
|
+
const text = [
|
|
234
|
+
'FULLSCREEN_TUI_DETECTED: this terminal is running a fullscreen program (e.g. vim/less/top); its screen output is not readable here.',
|
|
235
|
+
'Suggestions:',
|
|
236
|
+
'1. Run such programs in a native herdr pane where the human can interact directly.',
|
|
237
|
+
'2. Use non-interactive alternatives (cat/grep instead of less; edit/write tools instead of vim).',
|
|
238
|
+
`Raw head (first 200 chars): ${head}`,
|
|
239
|
+
].join('\n');
|
|
240
|
+
return { content: [{ type: 'text', text }], details: { error: 'FULLSCREEN_TUI_DETECTED' } };
|
|
241
|
+
}
|
|
242
|
+
const prev: ReadCursor | null = entry
|
|
243
|
+
? (entry.readRevision != null
|
|
244
|
+
? { revision: entry.readRevision, len: entry.readLen, tail: entry.readTail, eoTail: entry.readEoTail }
|
|
245
|
+
: null)
|
|
246
|
+
: null;
|
|
247
|
+
const inc = computeIncrement(prev, { text: read.text, revision: read.revision }, maxChars);
|
|
248
|
+
if (entry) {
|
|
249
|
+
entry.readRevision = inc.cursor.revision;
|
|
250
|
+
entry.readLen = inc.cursor.len;
|
|
251
|
+
entry.readTail = inc.cursor.tail;
|
|
252
|
+
entry.readEoTail = inc.cursor.eoTail;
|
|
287
253
|
touchTerminal(entry);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
254
|
+
}
|
|
255
|
+
if (inc.mode === 'none') {
|
|
256
|
+
return { content: [{ type: 'text', text: 'no new output' }], details: { mode: 'none', revision: read.revision } };
|
|
257
|
+
}
|
|
258
|
+
const output = stripAnsi(inc.text);
|
|
259
|
+
const text = output.length ? output : '(empty)';
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: 'text', text }],
|
|
262
|
+
details: { mode: inc.mode, revision: read.revision, hard_capped: inc.hardCapped, source_revision_truncated: read.truncated },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
291
265
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (!entry) return errText(`unknown terminal "${String(id)}" (see terminal_list)`);
|
|
306
|
-
if (entry.status === 'open') {
|
|
307
|
-
try {
|
|
308
|
-
await client.closePane(entry.paneId);
|
|
309
|
-
} catch {
|
|
310
|
-
/* The pane may have been closed by a person or with its tab; record the terminal as closed anyway. */
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
terminals = closeTerminal(terminals, entry.terminalId, Date.now()).entries;
|
|
314
|
-
persistTerminals();
|
|
315
|
-
return { content: [{ type: 'text', text: `terminal ${entry.terminalId} closed` }], details: { terminal_id: entry.terminalId } };
|
|
316
|
-
},
|
|
317
|
-
});
|
|
266
|
+
async function executeTerminalSignal(params) {
|
|
267
|
+
const entry = findOpenTerminal(params?.terminal_id);
|
|
268
|
+
if (!entry) return errText(`unknown or closed terminal "${String(params?.terminal_id)}" (see action list)`);
|
|
269
|
+
const v = validateSignal(typeof params?.key === 'string' ? params.key : '');
|
|
270
|
+
if (!v.ok) return errText(v.error);
|
|
271
|
+
try {
|
|
272
|
+
await client.sendPaneKeys(entry.paneId, [v.key]);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
return errText(`signal failed (pane may be closed): ${(e as Error).message}`);
|
|
275
|
+
}
|
|
276
|
+
touchTerminal(entry);
|
|
277
|
+
return { content: [{ type: 'text', text: `signal ${v.key} sent to ${entry.terminalId}` }], details: { terminal_id: entry.terminalId, key: v.key } };
|
|
278
|
+
}
|
|
318
279
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
'Entries whose pane is gone (human closed it, restart) are marked closed — terminal sessions do not survive pane closure or restarts; persist state to files instead of relying on terminal reuse.',
|
|
325
|
-
].join(' '),
|
|
326
|
-
parameters: Type.Object({}),
|
|
327
|
-
async execute() {
|
|
328
|
-
let livePaneIds: string[] = [];
|
|
280
|
+
async function executeTerminalClose(params) {
|
|
281
|
+
const id = params?.terminal_id;
|
|
282
|
+
const entry = terminals.find((t) => t.terminalId === id);
|
|
283
|
+
if (!entry) return errText(`unknown terminal "${String(id)}" (see action list)`);
|
|
284
|
+
if (entry.status === 'open') {
|
|
329
285
|
try {
|
|
330
|
-
|
|
286
|
+
await client.closePane(entry.paneId);
|
|
331
287
|
} catch {
|
|
332
|
-
/*
|
|
333
|
-
}
|
|
334
|
-
const s = summarizeSessions(terminals, livePaneIds);
|
|
335
|
-
// T6 check 3: record vanished panes as closed across restarts rather than reviving them; tell the user to persist state.
|
|
336
|
-
if (s.stalePaneIds.length > 0) {
|
|
337
|
-
terminals = terminals.map((t) =>
|
|
338
|
-
s.stalePaneIds.includes(t.paneId)
|
|
339
|
-
? { ...t, status: 'closed' as const, closedAt: t.closedAt ?? Date.now() }
|
|
340
|
-
: t,
|
|
341
|
-
);
|
|
342
|
-
persistTerminals();
|
|
288
|
+
/* The pane may have been closed by a person or with its tab; record the terminal as closed anyway. */
|
|
343
289
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
290
|
+
}
|
|
291
|
+
terminals = closeTerminal(terminals, entry.terminalId, Date.now()).entries;
|
|
292
|
+
persistTerminals();
|
|
293
|
+
return { content: [{ type: 'text', text: `terminal ${entry.terminalId} closed` }], details: { terminal_id: entry.terminalId } };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function executeTerminalList() {
|
|
297
|
+
let livePaneIds: string[] = [];
|
|
298
|
+
try {
|
|
299
|
+
livePaneIds = (await client.listPanes()).map((p) => p.paneId);
|
|
300
|
+
} catch {
|
|
301
|
+
/* Treat a failed query as unknown rather than closed, because an empty live set only affects stale detection. */
|
|
302
|
+
}
|
|
303
|
+
const s = summarizeSessions(terminals, livePaneIds);
|
|
304
|
+
// T6 check 3: record vanished panes as closed across restarts rather than reviving them; tell the user to persist state.
|
|
305
|
+
if (s.stalePaneIds.length > 0) {
|
|
306
|
+
terminals = terminals.map((t) =>
|
|
307
|
+
s.stalePaneIds.includes(t.paneId)
|
|
308
|
+
? { ...t, status: 'closed' as const, closedAt: t.closedAt ?? Date.now() }
|
|
309
|
+
: t,
|
|
349
310
|
);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return { content: [{ type: 'text', text:
|
|
354
|
-
}
|
|
355
|
-
|
|
311
|
+
persistTerminals();
|
|
312
|
+
}
|
|
313
|
+
if (s.terminals.length === 0) {
|
|
314
|
+
return { content: [{ type: 'text', text: 'no terminals (open one with action open)' }], details: { terminals: [] } };
|
|
315
|
+
}
|
|
316
|
+
const lines = s.terminals.map((t) =>
|
|
317
|
+
`- ${t.terminalId} [${t.live ? 'open' : 'closed'}] pane=${t.paneId} cwd=${t.cwd}${t.label ? ` (${t.label})` : ''}`,
|
|
318
|
+
);
|
|
319
|
+
const note = s.stalePaneIds.length > 0
|
|
320
|
+
? '\nnote: terminal sessions do not survive pane closure/restart — persist results to files.'
|
|
321
|
+
: '';
|
|
322
|
+
return { content: [{ type: 'text', text: lines.join('\n') + note }], details: { terminals: s.terminals } };
|
|
323
|
+
}
|
|
356
324
|
}
|