@ian-pascoe/pi-termctrl 0.4.0 → 0.4.1
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 +1 -1
- package/package.json +1 -1
- package/src/termctrl-driver.ts +40 -17
- package/src/terminal-tools.ts +5 -4
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ another fails and lists the caller's live entries.
|
|
|
92
92
|
`terminal_stop` stops a running entry (termctrl stop, then `SIGKILL` of the
|
|
93
93
|
Terminal's process group if it is still alive after 3 s) and forgets it. For a
|
|
94
94
|
Terminal it returns the final screen and scrolled-off lines; for a Background
|
|
95
|
-
job, its recent output. When a Terminal
|
|
95
|
+
job, its recent output. When a Terminal's screen is
|
|
96
96
|
unchanged since the agent's last result, the result omits the screen and reports
|
|
97
97
|
`changed: false` with `state` and `exit_code` or `signal`; `scrolled_off` still
|
|
98
98
|
appears when lines scrolled off since that result. Exited entries stay
|
package/package.json
CHANGED
package/src/termctrl-driver.ts
CHANGED
|
@@ -36,19 +36,38 @@ async function readPid(pidFile: string): Promise<number | undefined> {
|
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The requests made to one driver. termctrl answers every request sent before its shutdown request
|
|
41
|
+
* and none sent after it, and the SDK sends a request while its close is in flight without ever
|
|
42
|
+
* failing it, so that request would wait forever. The registry closes an idle driver without
|
|
43
|
+
* waiting, and a tool call may still read one of its exited Terminals, as `terminal_stop` does.
|
|
44
|
+
* Requests are therefore refused once closing starts, with the SDK's message for a closed driver.
|
|
45
|
+
*/
|
|
46
|
+
class DriverRequests {
|
|
47
|
+
private closing = false;
|
|
48
|
+
|
|
49
|
+
send<T>(request: () => Promise<T>): Promise<T> {
|
|
50
|
+
return this.closing ? Promise.reject(new Error("termctrl driver is closed")) : request();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
close(shutdown: () => Promise<void>): Promise<void> {
|
|
54
|
+
this.closing = true;
|
|
55
|
+
return shutdown();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
class TermctrlTerminal implements TerminalHandle {
|
|
40
60
|
constructor(
|
|
41
61
|
private readonly session: Session,
|
|
62
|
+
private readonly requests: DriverRequests,
|
|
42
63
|
private readonly pid: number | undefined,
|
|
43
64
|
) {}
|
|
44
65
|
|
|
45
66
|
async snapshot(): Promise<TerminalSnapshot> {
|
|
46
|
-
const status = await this.session.status();
|
|
47
|
-
const capture = await this.
|
|
48
|
-
allowIncomplete: true,
|
|
49
|
-
|
|
50
|
-
deadlineMs: 0,
|
|
51
|
-
});
|
|
67
|
+
const status = await this.requests.send(() => this.session.status());
|
|
68
|
+
const capture = await this.requests.send(() =>
|
|
69
|
+
this.session.screen.capture({ allowIncomplete: true, settleMs: 0, deadlineMs: 0 }),
|
|
70
|
+
);
|
|
52
71
|
return {
|
|
53
72
|
screen: capture.text,
|
|
54
73
|
state: status.state,
|
|
@@ -58,19 +77,19 @@ class TermctrlTerminal implements TerminalHandle {
|
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
logs(): Promise<string> {
|
|
61
|
-
return this.session.logs.text();
|
|
80
|
+
return this.requests.send(() => this.session.logs.text());
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
type(text: string): Promise<void> {
|
|
65
|
-
return this.session.keyboard.type(text);
|
|
84
|
+
return this.requests.send(() => this.session.keyboard.type(text));
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
press(keys: readonly Key[]): Promise<void> {
|
|
69
|
-
return this.session.keyboard.sequence(keys);
|
|
88
|
+
return this.requests.send(() => this.session.keyboard.sequence(keys));
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
stop(): Promise<void> {
|
|
73
|
-
return this.session.stop();
|
|
92
|
+
return this.requests.send(() => this.session.stop());
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
isAlive(): boolean {
|
|
@@ -98,6 +117,8 @@ class TermctrlTerminal implements TerminalHandle {
|
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
class TermctrlDriver implements TerminalDriver {
|
|
120
|
+
private readonly requests = new DriverRequests();
|
|
121
|
+
|
|
101
122
|
constructor(private readonly control: TerminalControl) {}
|
|
102
123
|
|
|
103
124
|
async launch(request: TerminalLaunchRequest): Promise<TerminalHandle> {
|
|
@@ -105,19 +126,21 @@ class TermctrlDriver implements TerminalDriver {
|
|
|
105
126
|
await mkdir(directory, { recursive: true });
|
|
106
127
|
const pidFile = join(directory, `${process.pid}-${request.id}.pid`);
|
|
107
128
|
try {
|
|
108
|
-
const session = await this.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
const session = await this.requests.send(() =>
|
|
130
|
+
this.control.launch({
|
|
131
|
+
command: ["/bin/sh", "-c", PID_WRAPPER, "pi-termctrl", pidFile, ...request.command],
|
|
132
|
+
cwd: request.cwd,
|
|
133
|
+
viewport: request.viewport,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
return new TermctrlTerminal(session, this.requests, await readPid(pidFile));
|
|
114
137
|
} finally {
|
|
115
138
|
await rm(pidFile, { force: true });
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
|
|
119
142
|
close(): Promise<void> {
|
|
120
|
-
return this.control.close();
|
|
143
|
+
return this.requests.close(() => this.control.close());
|
|
121
144
|
}
|
|
122
145
|
}
|
|
123
146
|
|
package/src/terminal-tools.ts
CHANGED
|
@@ -140,7 +140,7 @@ const StopResultSchema = Type.Object({
|
|
|
140
140
|
screen: Type.Optional(
|
|
141
141
|
Type.String({
|
|
142
142
|
description:
|
|
143
|
-
"Terminal only: the final screen; omitted
|
|
143
|
+
"Terminal only: the final screen; omitted when unchanged since your previous result",
|
|
144
144
|
}),
|
|
145
145
|
),
|
|
146
146
|
scrolled_off: Type.Optional(
|
|
@@ -815,10 +815,11 @@ export function createTerminalStopTool(registry: TermctrlRegistry) {
|
|
|
815
815
|
const parts = [header];
|
|
816
816
|
if (entry.kind === "terminal") {
|
|
817
817
|
const screen = entry.finalScreen ?? "";
|
|
818
|
-
|
|
819
|
-
|
|
818
|
+
const changed = previousScreen !== screen;
|
|
819
|
+
// A screen the agent already saw is not repeated, whether the Terminal was running or exited.
|
|
820
|
+
const repeated = !changed;
|
|
820
821
|
const output = await fitOutput(registry, entry, repeated ? "" : screen, scrolled);
|
|
821
|
-
result.changed =
|
|
822
|
+
result.changed = changed;
|
|
822
823
|
if (!repeated) result.screen = output.screen;
|
|
823
824
|
if (!repeated || output.scrolledOff !== "") result.scrolled_off = output.scrolledOff;
|
|
824
825
|
if (output.gap) result.output_missing = true;
|