@ian-pascoe/pi-termctrl 0.5.0 → 0.7.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.
- package/README.md +32 -19
- package/package.json +4 -3
- package/skills/pi-termctrl/SKILL.md +1 -1
- package/src/bash-replacement.ts +12 -8
- package/src/exit-notification.ts +55 -10
- package/src/pi-termctrl-extension.ts +27 -5
- package/src/pi-termctrl-settings.ts +66 -37
- package/src/ps-panel.ts +81 -35
- package/src/termctrl-registry.ts +7 -1
- package/src/terminal-render.ts +398 -0
- package/src/terminal-tools.ts +199 -45
package/README.md
CHANGED
|
@@ -64,10 +64,11 @@ code or signal; a poll (neither `text` nor `keys`) still returns its final scree
|
|
|
64
64
|
|
|
65
65
|
Results contain the visible screen, the log lines that scrolled off since the
|
|
66
66
|
agent's previous result, `state`, `exit_code` or `signal` once exited, and
|
|
67
|
-
`changed: false` when the screen matches the previous result.
|
|
68
|
-
|
|
69
|
-
such as a progress line
|
|
70
|
-
|
|
67
|
+
`changed: false` when the screen matches the previous result. A line the agent
|
|
68
|
+
saw on an earlier screen is not repeated when it scrolls off unchanged; a line
|
|
69
|
+
rewritten in place since, such as a progress line or a prompt the agent typed
|
|
70
|
+
at, arrives again in its final form. Tools that declare structured output give
|
|
71
|
+
`codemode` scripts typed values.
|
|
71
72
|
|
|
72
73
|
Scrolled-off lines are best effort: they come from termctrl's scrollback, which
|
|
73
74
|
is limited (several hundred lines; fewer when lines are long) and drops its
|
|
@@ -80,11 +81,14 @@ scrollback holds before that result, its oldest lines are lost without
|
|
|
80
81
|
`output_missing`, because termctrl does not report the trimming. Run commands
|
|
81
82
|
whose complete output matters with `bash`, or redirect their output to a file.
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
Scrolled-off lines are limited by `termctrl.scrollback` (100 lines or 16 KB by
|
|
85
|
+
default). Past it, a result keeps the first and last lines and puts a
|
|
86
|
+
`[… N lines omitted …]` line between them. A whole result is limited to Pi's
|
|
87
|
+
2000 lines or 50 KB: the screen is kept first, from its bottom, and the
|
|
88
|
+
scrolled-off lines fill the rest. With `scrollback` set to `false`, only Pi's
|
|
89
|
+
limits apply and the newest scrolled-off lines are kept. When anything is cut,
|
|
90
|
+
every line of the result goes to `$TMPDIR/pi-termctrl/<pid>-<id>-output-<n>.log`,
|
|
91
|
+
named in the result's notice and `full_output_path`.
|
|
88
92
|
|
|
89
93
|
`keys` are termctrl key names: `Enter`, `Escape`, `ArrowUp`, `ArrowDown`,
|
|
90
94
|
`ArrowLeft`, `ArrowRight`, `Tab`, `Shift+Tab`, `Backspace`, `Delete`, `Home`,
|
|
@@ -185,9 +189,16 @@ the session that started the entry, and wait across `/reload`.
|
|
|
185
189
|
`/ps` opens an overlay listing every entry in the Pi process, including those of
|
|
186
190
|
in-process child agents, with kind, id, owner (`root` or `child`), state and
|
|
187
191
|
age, plus a live preview of the selected Terminal's screen or Background job's
|
|
188
|
-
log tail.
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
log tail. It uses Pi's selector frame, with a Status Mark before each entry
|
|
193
|
+
(`●` running, `✓` exited cleanly, `✗` failed, `■` ended by a signal). Keys:
|
|
194
|
+
`↑`/`↓` select, `k` stops without confirmation (the agent gets an Exit
|
|
195
|
+
notification), `x` removes an exited entry, `Esc` closes. The footer shows
|
|
196
|
+
`● termctrl N running` while anything is live.
|
|
197
|
+
|
|
198
|
+
The `terminal_*` tools render like `bash` in the transcript: a header with the
|
|
199
|
+
tool name and target, the last 5 screen lines (`ctrl+o` expands), and a `Took`
|
|
200
|
+
footer. Exit notifications show one row per exit, marked `✓` for a clean exit and
|
|
201
|
+
`■` otherwise. In RPC mode `/ps` sends a one-line summary;
|
|
191
202
|
print mode has no UI but keeps the tools and notifications.
|
|
192
203
|
|
|
193
204
|
## Lifecycle
|
|
@@ -214,17 +225,19 @@ by field:
|
|
|
214
225
|
"replaceBash": true,
|
|
215
226
|
"defaultViewport": { "cols": 120, "rows": 40 },
|
|
216
227
|
"exitTailLines": 20,
|
|
217
|
-
"bashTail": { "lines": 300, "bytes": 16384 }
|
|
228
|
+
"bashTail": { "lines": 300, "bytes": 16384 },
|
|
229
|
+
"scrollback": { "lines": 100, "bytes": 16384 }
|
|
218
230
|
}
|
|
219
231
|
}
|
|
220
232
|
```
|
|
221
233
|
|
|
222
|
-
| Setting | Default | Meaning
|
|
223
|
-
| ----------------- | ------------------------------ |
|
|
224
|
-
| `replaceBash` | `true` | Replace `bash` to enable Background jobs
|
|
225
|
-
| `defaultViewport` | `{ cols: 120, rows: 40 }` | Terminal size; each dimension 1 to 1000
|
|
226
|
-
| `exitTailLines` | `20` | Lines of output in an Exit notification, 0–1000
|
|
227
|
-
| `bashTail` | `{ lines: 300, bytes: 16384 }` | Model-visible `bash` output tail: `lines` 1–2000, `bytes` 1–51200; `false` or `0` restores Pi's limits
|
|
234
|
+
| Setting | Default | Meaning |
|
|
235
|
+
| ----------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
236
|
+
| `replaceBash` | `true` | Replace `bash` to enable Background jobs |
|
|
237
|
+
| `defaultViewport` | `{ cols: 120, rows: 40 }` | Terminal size; each dimension 1 to 1000 |
|
|
238
|
+
| `exitTailLines` | `20` | Lines of output in an Exit notification, 0–1000 |
|
|
239
|
+
| `bashTail` | `{ lines: 300, bytes: 16384 }` | Model-visible `bash` output tail: `lines` 1–2000, `bytes` 1–51200; `false` or `0` restores Pi's limits |
|
|
240
|
+
| `scrollback` | `{ lines: 100, bytes: 16384 }` | Scrolled-off lines in a Terminal result, keeping the first and last; same ranges and opt-out as `bashTail` |
|
|
228
241
|
|
|
229
242
|
Invalid values keep the lower layer and unknown keys produce warnings at session
|
|
230
243
|
start. Use `/reload` after editing settings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-termctrl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Interactive Terminals, Background jobs, and a /ps panel for Pi.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"provenance": true
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@ian-pascoe/pi-utils": "^0.5.0",
|
|
37
38
|
"@kitlangton/terminal-control": "^1.2.1"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
41
|
-
"@earendil-works/pi-tui": ">=
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.1.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=1.1.0",
|
|
42
43
|
"typebox": "*"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
|
@@ -11,7 +11,7 @@ disable-model-invocation: true
|
|
|
11
11
|
2. If `terminal_start` is missing, check the startup diagnostic. Confirm the platform has a packaged `termctrl` binary or that `TERMCTRL_BINARY` names an executable one.
|
|
12
12
|
3. If `bash` lacks the `background` parameter, check `termctrl.replaceBash` and look for another extension that also registers `bash`.
|
|
13
13
|
4. Call `terminal_list` to inspect live and exited Terminals and Background jobs. Ask the user to open `/ps` for a live preview.
|
|
14
|
-
5. If `bash` results are shorter than expected, check `termctrl.bashTail` (default 300 lines or 16 KB; `false` restores Pi's limits); the result's notice names the limits and the file holding every line.
|
|
14
|
+
5. If `bash` results are shorter than expected, check `termctrl.bashTail` (default 300 lines or 16 KB; `false` restores Pi's limits); the result's notice names the limits and the file holding every line. If a Terminal result's `scrolled_off` shows `[… N lines omitted …]`, check `termctrl.scrollback` (default 100 lines or 16 KB) the same way.
|
|
15
15
|
6. Classify the failure as settings, binary resolution, `bash` conflict, cap, or process behaviour.
|
|
16
16
|
7. For a requested change, edit one settings layer, validate its JSON, and reload Pi. Finish when the expected tools appear or one exact cause remains.
|
|
17
17
|
|
package/src/bash-replacement.ts
CHANGED
|
@@ -19,9 +19,10 @@ import {
|
|
|
19
19
|
type TruncationResult,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
22
|
+
import { noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
22
23
|
import { Type, type TSchema } from "typebox";
|
|
23
24
|
import { KILLED_EXIT, TermctrlRegistry } from "./termctrl-registry.js";
|
|
24
|
-
import type {
|
|
25
|
+
import type { LineByteLimits } from "./pi-termctrl-settings.js";
|
|
25
26
|
import { termctrlTemporaryDirectory } from "./termctrl-driver.js";
|
|
26
27
|
import type { TerminalExit } from "./terminal-driver.js";
|
|
27
28
|
|
|
@@ -264,7 +265,10 @@ export class RunningBashCalls {
|
|
|
264
265
|
try {
|
|
265
266
|
call.background(this.registry());
|
|
266
267
|
} catch (error) {
|
|
267
|
-
ui.notify(
|
|
268
|
+
ui.notify(
|
|
269
|
+
noticeText("Termctrl", error instanceof Error ? error.message : String(error)),
|
|
270
|
+
"warning",
|
|
271
|
+
);
|
|
268
272
|
}
|
|
269
273
|
}
|
|
270
274
|
}
|
|
@@ -286,7 +290,7 @@ export interface BashReplacementOptions {
|
|
|
286
290
|
* Limits on the model-visible tail of a result and of "output so far"; undefined keeps Pi's own
|
|
287
291
|
* limits. Pi's `bash` accepts no limits, so the replacement tightens what Pi returns.
|
|
288
292
|
*/
|
|
289
|
-
readonly bashTail?:
|
|
293
|
+
readonly bashTail?: LineByteLimits | undefined;
|
|
290
294
|
/** Builds Pi's `bash` definition; tests wrap it to observe what reaches Pi's accumulator. */
|
|
291
295
|
readonly definitionFactory?: typeof createBashToolDefinition;
|
|
292
296
|
}
|
|
@@ -377,7 +381,7 @@ function limitForegroundText(
|
|
|
377
381
|
call: BashCall,
|
|
378
382
|
piText: string,
|
|
379
383
|
piDetails: BashToolDetails | undefined,
|
|
380
|
-
limits:
|
|
384
|
+
limits: LineByteLimits,
|
|
381
385
|
hasStatus: boolean,
|
|
382
386
|
) {
|
|
383
387
|
const output = call.foregroundOutput();
|
|
@@ -403,7 +407,7 @@ function limitForegroundText(
|
|
|
403
407
|
function backgroundResult(
|
|
404
408
|
outcome: BackgroundOutcome,
|
|
405
409
|
startedAt: number,
|
|
406
|
-
limits:
|
|
410
|
+
limits: LineByteLimits | undefined,
|
|
407
411
|
) {
|
|
408
412
|
const truncation = truncateBuffered(outcome.output, outcome.dropped, limits ?? {});
|
|
409
413
|
let text = truncation.content;
|
|
@@ -427,7 +431,7 @@ function backgroundResult(
|
|
|
427
431
|
|
|
428
432
|
type BashResult = Awaited<ReturnType<ReturnType<typeof createBashToolDefinition>["execute"]>>;
|
|
429
433
|
|
|
430
|
-
function limitResult(call: BashCall, result: BashResult, limits:
|
|
434
|
+
function limitResult(call: BashCall, result: BashResult, limits: LineByteLimits): BashResult {
|
|
431
435
|
const [first] = result.content;
|
|
432
436
|
if (first?.type !== "text") return result;
|
|
433
437
|
const limited = limitForegroundText(
|
|
@@ -445,7 +449,7 @@ function limitResult(call: BashCall, result: BashResult, limits: BashTailLimits)
|
|
|
445
449
|
};
|
|
446
450
|
}
|
|
447
451
|
|
|
448
|
-
function limitError(call: BashCall, error: Error, limits:
|
|
452
|
+
function limitError(call: BashCall, error: Error, limits: LineByteLimits): Error {
|
|
449
453
|
// Only Pi's output-plus-status messages are rewritten; any other error passes through.
|
|
450
454
|
if (!PI_STATUS.test(error.message)) return error;
|
|
451
455
|
const limited = limitForegroundText(call, error.message, undefined, limits, true);
|
|
@@ -453,7 +457,7 @@ function limitError(call: BashCall, error: Error, limits: BashTailLimits): Error
|
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
/** Pi's description names its own limits; name the ones in force instead. */
|
|
456
|
-
function describeTail(description: string, limits:
|
|
460
|
+
function describeTail(description: string, limits: LineByteLimits | undefined): string {
|
|
457
461
|
if (limits === undefined) return description;
|
|
458
462
|
const piLimits = `${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB`;
|
|
459
463
|
const ours = `${limits.maxLines} lines or ${formatSize(limits.maxBytes)}`;
|
package/src/exit-notification.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { MessageRenderer } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
COLLAPSED_LINES,
|
|
5
|
+
customMessageBox,
|
|
6
|
+
expandHint,
|
|
7
|
+
joinInline,
|
|
8
|
+
previewBody,
|
|
9
|
+
statusMark,
|
|
10
|
+
summaryExpandHint,
|
|
11
|
+
} from "@ian-pascoe/pi-utils/ui";
|
|
3
12
|
import { Type, type Static } from "typebox";
|
|
4
13
|
import { Value } from "typebox/value";
|
|
5
14
|
import type { ExitNotice } from "./termctrl-registry.js";
|
|
@@ -110,19 +119,55 @@ function messageText(content: RenderedMessage["content"]): string {
|
|
|
110
119
|
return content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
type MessageTheme = Parameters<ExitNotificationRenderer>[2];
|
|
123
|
+
|
|
124
|
+
function exitRow(theme: MessageTheme, exit: ExitNotificationDetails["exits"][number]): string {
|
|
125
|
+
const done = exit.exit_code === 0 && exit.signal === null;
|
|
126
|
+
const body = (text: string) => theme.fg("customMessageText", text);
|
|
127
|
+
const status = describeExitStatus({ code: exit.exit_code, signal: exit.signal });
|
|
128
|
+
return `${statusMark(theme, done ? "done" : "stopped")} ${body(theme.bold(exit.id))} ${joinInline(
|
|
129
|
+
theme,
|
|
130
|
+
[body(status), body(formatDuration(exit.duration_ms)), theme.fg("muted", exit.command)],
|
|
131
|
+
)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The Collapsed View's rows: at most 10, then the Expand Hint for the rest or for the full text. */
|
|
135
|
+
function collapsedRows(theme: MessageTheme, rows: readonly string[]): string[] {
|
|
136
|
+
const hidden = rows.length - COLLAPSED_LINES.fallback;
|
|
137
|
+
if (hidden > 0) return [...rows.slice(0, COLLAPSED_LINES.fallback), expandHint(theme, hidden)];
|
|
138
|
+
const last = rows.length - 1;
|
|
139
|
+
return rows.map((row, index) => (index === last ? `${row}${summaryExpandHint(theme)}` : row));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `exit`, or `N exits` when several Terminals or Background jobs ended together. */
|
|
143
|
+
function exitHeading(details: ExitNotificationDetails | undefined): string {
|
|
144
|
+
const count = Value.Check(ExitNotificationDetailsSchema, details) ? details.exits.length : 1;
|
|
145
|
+
return count === 1 ? "exit" : `${count} exits`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pi's custom-message look: a `customMessageBg` box under a bold `[termctrl] exit` label.
|
|
150
|
+
* Collapsed, it shows one Status Mark row per exit; expanded, the full text.
|
|
151
|
+
*/
|
|
114
152
|
export const renderExitNotification: ExitNotificationRenderer = (
|
|
115
153
|
message,
|
|
116
154
|
options,
|
|
117
155
|
theme,
|
|
118
156
|
): Component => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
const details = message.details;
|
|
158
|
+
const rows =
|
|
159
|
+
!options.expanded && Value.Check(ExitNotificationDetailsSchema, details)
|
|
160
|
+
? collapsedRows(
|
|
161
|
+
theme,
|
|
162
|
+
details.exits.map((exit) => exitRow(theme, exit)),
|
|
163
|
+
)
|
|
164
|
+
: previewBody(theme, messageText(message.content).replace(/\n+$/u, "").split("\n"), {
|
|
165
|
+
limit: COLLAPSED_LINES.fallback,
|
|
166
|
+
expanded: options.expanded,
|
|
167
|
+
color: "customMessageText",
|
|
168
|
+
});
|
|
169
|
+
const heading = theme.fg("customMessageText", exitHeading(details));
|
|
170
|
+
return customMessageBox(theme, { outputPad: options.outputPad, source: "termctrl", heading }, [
|
|
171
|
+
new Text(rows.join("\n"), 0, 0),
|
|
172
|
+
]);
|
|
128
173
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ExtensionFactory,
|
|
8
8
|
type SessionShutdownEvent,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { expandMessageOnClick, noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
10
11
|
import { createBashReplacement, RunningBashCalls } from "./bash-replacement.js";
|
|
11
12
|
import {
|
|
12
13
|
EXIT_NOTIFICATION_TYPE,
|
|
@@ -17,7 +18,11 @@ import { TermctrlPsController } from "./ps-panel.js";
|
|
|
17
18
|
import { resolveTermctrlBinary, type TermctrlBinaryResolution } from "./termctrl-binary.js";
|
|
18
19
|
import { createTermctrlDriver } from "./termctrl-driver.js";
|
|
19
20
|
import { TermctrlRegistry } from "./termctrl-registry.js";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
resolveTermctrlSettings,
|
|
23
|
+
type LineByteLimits,
|
|
24
|
+
type TerminalViewport,
|
|
25
|
+
} from "./pi-termctrl-settings.js";
|
|
21
26
|
import type { TerminalDriver } from "./terminal-driver.js";
|
|
22
27
|
import {
|
|
23
28
|
createTerminalListTool,
|
|
@@ -41,6 +46,7 @@ interface ActiveSession {
|
|
|
41
46
|
readonly ps: TermctrlPsController;
|
|
42
47
|
readonly shell: () => TerminalShell;
|
|
43
48
|
readonly viewport: TerminalViewport;
|
|
49
|
+
readonly scrollback: LineByteLimits | undefined;
|
|
44
50
|
readonly exitTailLines: number;
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -79,7 +85,10 @@ class PiTermctrlController {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
register(): void {
|
|
82
|
-
this.pi.registerMessageRenderer(
|
|
88
|
+
this.pi.registerMessageRenderer(
|
|
89
|
+
EXIT_NOTIFICATION_TYPE,
|
|
90
|
+
expandMessageOnClick(renderExitNotification),
|
|
91
|
+
);
|
|
83
92
|
this.pi.registerCommand("ps", {
|
|
84
93
|
description: "Show and stop Terminals and Background jobs",
|
|
85
94
|
handler: async () => {
|
|
@@ -91,6 +100,7 @@ class PiTermctrlController {
|
|
|
91
100
|
registry: this.registry,
|
|
92
101
|
shell: () => this.session().shell(),
|
|
93
102
|
viewport: () => this.session().viewport,
|
|
103
|
+
scrollback: () => this.session().scrollback,
|
|
94
104
|
};
|
|
95
105
|
this.pi.registerTool(createTerminalStartTool(runtime));
|
|
96
106
|
this.pi.registerTool(createTerminalSendTool(runtime));
|
|
@@ -120,7 +130,12 @@ class PiTermctrlController {
|
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
private registerManagementTools(): void {
|
|
123
|
-
this.pi.registerTool(
|
|
133
|
+
this.pi.registerTool(
|
|
134
|
+
createTerminalStopTool({
|
|
135
|
+
registry: this.registry,
|
|
136
|
+
scrollback: () => this.session().scrollback,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
124
139
|
this.pi.registerTool(createTerminalListTool(this.registry));
|
|
125
140
|
this.pi.registerTool(
|
|
126
141
|
createTerminalWaitTool({
|
|
@@ -143,7 +158,10 @@ class PiTermctrlController {
|
|
|
143
158
|
);
|
|
144
159
|
const settings = resolveTermctrlSettings(settingsManager);
|
|
145
160
|
if (settings.warnings.length > 0) {
|
|
146
|
-
context.ui.notify(
|
|
161
|
+
context.ui.notify(
|
|
162
|
+
noticeText("Termctrl", `Settings:\n- ${settings.warnings.join("\n- ")}`),
|
|
163
|
+
"warning",
|
|
164
|
+
);
|
|
147
165
|
}
|
|
148
166
|
const registry = this.registry;
|
|
149
167
|
const owner = context.sessionManager.getSessionId();
|
|
@@ -167,7 +185,10 @@ class PiTermctrlController {
|
|
|
167
185
|
const commandPrefix = settingsManager.getShellCommandPrefix();
|
|
168
186
|
if (this.binary.kind === "missing") {
|
|
169
187
|
context.ui.notify(
|
|
170
|
-
|
|
188
|
+
noticeText(
|
|
189
|
+
"Termctrl",
|
|
190
|
+
`Terminal tools are unavailable: ${this.binary.reason}\nRun /skill:pi-termctrl to diagnose.`,
|
|
191
|
+
),
|
|
171
192
|
"warning",
|
|
172
193
|
);
|
|
173
194
|
if (settings.replaceBash) this.registerManagementTools();
|
|
@@ -195,6 +216,7 @@ class PiTermctrlController {
|
|
|
195
216
|
return { shell: shell.shell, args: shell.args, commandPrefix };
|
|
196
217
|
},
|
|
197
218
|
viewport: settings.defaultViewport,
|
|
219
|
+
scrollback: settings.scrollback,
|
|
198
220
|
exitTailLines: settings.exitTailLines,
|
|
199
221
|
};
|
|
200
222
|
}
|
|
@@ -20,16 +20,24 @@ export interface TerminalViewport {
|
|
|
20
20
|
readonly rows: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** The most lines and bytes of
|
|
24
|
-
export type
|
|
23
|
+
/** The most lines and bytes of model-visible output that a setting allows. */
|
|
24
|
+
export type LineByteLimits = Readonly<Required<TruncationOptions>>;
|
|
25
25
|
|
|
26
26
|
/** The effective `settings.termctrl` values after layering global and trusted project settings. */
|
|
27
27
|
export interface ResolvedTermctrlSettings {
|
|
28
28
|
readonly replaceBash: boolean;
|
|
29
29
|
readonly defaultViewport: TerminalViewport;
|
|
30
30
|
readonly exitTailLines: number;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Limits on the model-visible `bash` output: a foreground result or the output so far of a
|
|
33
|
+
* backgrounding one. Undefined restores Pi's own limits.
|
|
34
|
+
*/
|
|
35
|
+
readonly bashTail: LineByteLimits | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Limits on the scrolled-off lines a Terminal result shows, kept from their start and end.
|
|
38
|
+
* Undefined restores Pi's own limits on the whole result.
|
|
39
|
+
*/
|
|
40
|
+
readonly scrollback: LineByteLimits | undefined;
|
|
33
41
|
readonly warnings: readonly string[];
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -44,31 +52,42 @@ export interface TermctrlSettingsReader {
|
|
|
44
52
|
getProjectSettings(): TermctrlSettingsDocumentInput;
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
/** The settings that limit model-visible output. */
|
|
56
|
+
type LineByteLimitKey = "bashTail" | "scrollback";
|
|
57
|
+
|
|
58
|
+
/** One layer's value for a `bashTail` or `scrollback` setting. */
|
|
59
|
+
interface LineByteLimitsLayer {
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
lines?: number;
|
|
62
|
+
bytes?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
interface TermctrlLayer {
|
|
48
66
|
replaceBash?: boolean;
|
|
49
67
|
cols?: number;
|
|
50
68
|
rows?: number;
|
|
51
69
|
exitTailLines?: number;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
bashTailBytes?: number;
|
|
70
|
+
bashTail?: LineByteLimitsLayer;
|
|
71
|
+
scrollback?: LineByteLimitsLayer;
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
export const DEFAULT_TERMCTRL_SETTINGS: Omit<
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
export const DEFAULT_TERMCTRL_SETTINGS: Omit<
|
|
75
|
+
ResolvedTermctrlSettings,
|
|
76
|
+
"warnings" | LineByteLimitKey
|
|
77
|
+
> & { readonly [key in LineByteLimitKey]: LineByteLimits } = {
|
|
60
78
|
replaceBash: true,
|
|
61
79
|
defaultViewport: { cols: 120, rows: 40 },
|
|
62
80
|
exitTailLines: 20,
|
|
63
81
|
bashTail: { maxLines: 300, maxBytes: 16 * 1024 },
|
|
82
|
+
scrollback: { maxLines: 100, maxBytes: 16 * 1024 },
|
|
64
83
|
};
|
|
65
84
|
|
|
66
85
|
const JsonObjectSchema = Type.Record(Type.String(), Type.Any());
|
|
67
86
|
const SettingsDocumentSchema = Type.Object({ termctrl: Type.Optional(Type.Any()) });
|
|
68
87
|
const BooleanSchema = Type.Boolean();
|
|
69
88
|
const ViewportDimensionSchema = Type.Integer({ minimum: 1, maximum: VIEWPORT_LIMIT });
|
|
70
|
-
const
|
|
71
|
-
const
|
|
89
|
+
const LimitLinesSchema = Type.Integer({ minimum: 1, maximum: DEFAULT_MAX_LINES });
|
|
90
|
+
const LimitBytesSchema = Type.Integer({ minimum: 1, maximum: DEFAULT_MAX_BYTES });
|
|
72
91
|
const ExitTailLinesSchema = Type.Integer({ minimum: 0, maximum: EXIT_TAIL_LIMIT });
|
|
73
92
|
|
|
74
93
|
function isJsonObject(value: JsonValue): value is JsonObject {
|
|
@@ -103,7 +122,8 @@ function readLayer(
|
|
|
103
122
|
else warnings.push(`${path}: expected an integer from 0 to ${EXIT_TAIL_LIMIT}`);
|
|
104
123
|
break;
|
|
105
124
|
case "bashTail":
|
|
106
|
-
|
|
125
|
+
case "scrollback":
|
|
126
|
+
layer[key] = readLineByteLimits(value, path, warnings);
|
|
107
127
|
break;
|
|
108
128
|
case "defaultViewport":
|
|
109
129
|
readViewport(value, path, layer, warnings);
|
|
@@ -130,29 +150,28 @@ function readViewport(value: JsonValue, path: string, layer: TermctrlLayer, warn
|
|
|
130
150
|
}
|
|
131
151
|
}
|
|
132
152
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (value
|
|
139
|
-
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
153
|
+
function readLineByteLimits(
|
|
154
|
+
value: JsonValue,
|
|
155
|
+
path: string,
|
|
156
|
+
warnings: string[],
|
|
157
|
+
): LineByteLimitsLayer {
|
|
158
|
+
if (Value.Check(BooleanSchema, value)) return { enabled: value };
|
|
159
|
+
if (value === 0) return { enabled: false };
|
|
142
160
|
if (!isJsonObject(value)) {
|
|
143
161
|
warnings.push(`${path}: expected a boolean, 0, or a JSON object`);
|
|
144
|
-
return;
|
|
162
|
+
return {};
|
|
145
163
|
}
|
|
164
|
+
const limits: LineByteLimitsLayer = {};
|
|
146
165
|
let valid = 0;
|
|
147
166
|
for (const [key, limit] of Object.entries(value)) {
|
|
148
167
|
if (key === "lines") {
|
|
149
|
-
if (Value.Check(
|
|
150
|
-
|
|
168
|
+
if (Value.Check(LimitLinesSchema, limit)) {
|
|
169
|
+
limits.lines = limit;
|
|
151
170
|
valid++;
|
|
152
171
|
} else warnings.push(`${path}.lines: expected an integer from 1 to ${DEFAULT_MAX_LINES}`);
|
|
153
172
|
} else if (key === "bytes") {
|
|
154
|
-
if (Value.Check(
|
|
155
|
-
|
|
173
|
+
if (Value.Check(LimitBytesSchema, limit)) {
|
|
174
|
+
limits.bytes = limit;
|
|
156
175
|
valid++;
|
|
157
176
|
} else warnings.push(`${path}.bytes: expected an integer from 1 to ${DEFAULT_MAX_BYTES}`);
|
|
158
177
|
} else {
|
|
@@ -160,7 +179,24 @@ function readBashTail(value: JsonValue, path: string, layer: TermctrlLayer, warn
|
|
|
160
179
|
}
|
|
161
180
|
}
|
|
162
181
|
// An object with only invalid fields must not override a lower layer's opt-out.
|
|
163
|
-
if (valid > 0 || Object.keys(value).length === 0)
|
|
182
|
+
if (valid > 0 || Object.keys(value).length === 0) limits.enabled = true;
|
|
183
|
+
return limits;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Layer a `bashTail` or `scrollback` setting: the project's fields over the global ones, over the defaults. */
|
|
187
|
+
function resolveLineByteLimits(
|
|
188
|
+
key: LineByteLimitKey,
|
|
189
|
+
globalLayer: TermctrlLayer,
|
|
190
|
+
projectLayer: TermctrlLayer,
|
|
191
|
+
): LineByteLimits | undefined {
|
|
192
|
+
const global = globalLayer[key];
|
|
193
|
+
const project = projectLayer[key];
|
|
194
|
+
if (!(project?.enabled ?? global?.enabled ?? true)) return undefined;
|
|
195
|
+
const defaults = DEFAULT_TERMCTRL_SETTINGS[key];
|
|
196
|
+
return {
|
|
197
|
+
maxLines: project?.lines ?? global?.lines ?? defaults.maxLines,
|
|
198
|
+
maxBytes: project?.bytes ?? global?.bytes ?? defaults.maxBytes,
|
|
199
|
+
};
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
/** Resolve global and trusted-project `termctrl` settings, keeping valid fields around warnings. */
|
|
@@ -177,15 +213,8 @@ export function resolveTermctrlSettings(reader: TermctrlSettingsReader): Resolve
|
|
|
177
213
|
},
|
|
178
214
|
exitTailLines:
|
|
179
215
|
projectLayer.exitTailLines ?? globalLayer.exitTailLines ?? defaults.exitTailLines,
|
|
180
|
-
bashTail:
|
|
181
|
-
|
|
182
|
-
? {
|
|
183
|
-
maxLines:
|
|
184
|
-
projectLayer.bashTailLines ?? globalLayer.bashTailLines ?? defaults.bashTail.maxLines,
|
|
185
|
-
maxBytes:
|
|
186
|
-
projectLayer.bashTailBytes ?? globalLayer.bashTailBytes ?? defaults.bashTail.maxBytes,
|
|
187
|
-
}
|
|
188
|
-
: undefined,
|
|
216
|
+
bashTail: resolveLineByteLimits("bashTail", globalLayer, projectLayer),
|
|
217
|
+
scrollback: resolveLineByteLimits("scrollback", globalLayer, projectLayer),
|
|
189
218
|
warnings,
|
|
190
219
|
};
|
|
191
220
|
}
|