pi-onlyne 1.2.1 → 2.0.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 +147 -240
- package/README.zh.md +79 -125
- package/package.json +2 -3
- package/src/activity.test.mjs +0 -6
- package/src/agent.live.test.mjs +15 -5
- package/src/agent.mjs +413 -291
- package/src/agent.test.mjs +466 -395
- package/src/background-subagents.mjs +192 -0
- package/src/background-subagents.test.mjs +166 -0
- package/src/config.mjs +4 -23
- package/src/config.test.mjs +0 -8
- package/src/index.ts +67 -54
- package/src/pi-surface.mjs +110 -30
- package/src/pi-surface.test.mjs +232 -0
- package/src/protocol.mjs +28 -47
- package/src/protocol.test.mjs +23 -69
- package/src/socket.mjs +270 -39
- package/src/socket.test.mjs +205 -47
- package/relay.toml.example +0 -18
- package/src/relay.mjs +0 -299
- package/src/relay.test.mjs +0 -210
package/src/index.ts
CHANGED
|
@@ -7,21 +7,26 @@
|
|
|
7
7
|
// `crates/onlyne-client/src/dispatch.rs`); with any of the three missing this is
|
|
8
8
|
// a plain pi session and the extension stays silent rather than failing.
|
|
9
9
|
//
|
|
10
|
-
// session_start
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
10
|
+
// session_start -> read env + .pi/onlyne.json, connect, register tools
|
|
11
|
+
// before_agent_start -> the role prose becomes one section of the system
|
|
12
|
+
// prompt the run is about to send (the instruction layer)
|
|
13
|
+
// turn_start -> heartbeat{running}
|
|
14
|
+
// turn_end -> one turn of a run ended; the phase is re-derived from pi
|
|
15
|
+
// and the fallback window for a witnessed failure opens
|
|
16
|
+
// message_end -> keep the last assistant text; a failed turn is `failed`
|
|
17
|
+
// agent_settled -> heartbeat{idle} when the session waits for input, and the
|
|
18
|
+
// report a failed turn owes is sent from here
|
|
19
|
+
// session_shutdown -> detach{reason}
|
|
20
|
+
//
|
|
21
|
+
// Host frames are dispatched in `agent.mjs`, not here: `assign` and `nudge` are
|
|
22
|
+
// injected as user messages, `probe` is answered with a heartbeat, and `recycle`
|
|
23
|
+
// settles the task and stops the plugin.
|
|
18
24
|
|
|
19
25
|
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
26
|
import { Type } from "typebox";
|
|
21
27
|
|
|
22
28
|
import { OnlyneAgent } from "./agent.mjs";
|
|
23
29
|
import { loadConfig, sessionIdentity } from "./config.mjs";
|
|
24
|
-
import { loadRelay, relayEnabled } from "./relay.mjs";
|
|
25
30
|
import { resolveSocketPath } from "./socket.mjs";
|
|
26
31
|
import { createSurface } from "./pi-surface.mjs";
|
|
27
32
|
|
|
@@ -54,7 +59,6 @@ interface WelcomeLike {
|
|
|
54
59
|
interface PiSurface {
|
|
55
60
|
available: {
|
|
56
61
|
wakeUser: boolean;
|
|
57
|
-
proseContext: boolean;
|
|
58
62
|
customEntry: boolean;
|
|
59
63
|
widget: boolean;
|
|
60
64
|
status: boolean;
|
|
@@ -64,7 +68,8 @@ interface PiSurface {
|
|
|
64
68
|
registerCommand: boolean;
|
|
65
69
|
};
|
|
66
70
|
wakeUser(text: string, parts?: ImagePartInput[]): boolean;
|
|
67
|
-
|
|
71
|
+
roleProse(text: string): boolean;
|
|
72
|
+
applyRoleProse(event: { systemPromptOptions?: { sections?: Record<string, string> } }): boolean;
|
|
68
73
|
customEntry(customType: string, data: unknown): boolean;
|
|
69
74
|
widget(lines: string[] | undefined): void;
|
|
70
75
|
status(text: string): void;
|
|
@@ -141,10 +146,10 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
141
146
|
name: "onlyne_send",
|
|
142
147
|
label: "Onlyne send",
|
|
143
148
|
description:
|
|
144
|
-
"Send one message to another role
|
|
145
|
-
promptSnippet: "Send a note or a task to another
|
|
149
|
+
"Send one message to another role. kind=note (default) is free text; kind=task hands work to that role and opens a task for it.",
|
|
150
|
+
promptSnippet: "Send a note or a task to another role",
|
|
146
151
|
promptGuidelines: [
|
|
147
|
-
"Use onlyne_send when
|
|
152
|
+
"Use onlyne_send when something has to reach another role; the call returns once the message is queued.",
|
|
148
153
|
],
|
|
149
154
|
parameters: Type.Object({
|
|
150
155
|
to: Type.String({ description: "target role name, e.g. builder" }),
|
|
@@ -160,7 +165,9 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
160
165
|
kind: params.kind,
|
|
161
166
|
imagePath: params.image ?? null,
|
|
162
167
|
});
|
|
163
|
-
|
|
168
|
+
// The recipient and nothing else: a tool result is model-visible, and
|
|
169
|
+
// there is no fact about this send the model needs beyond where it went.
|
|
170
|
+
return textResult(`sent to ${result.to}`, { to: result.to });
|
|
164
171
|
},
|
|
165
172
|
}));
|
|
166
173
|
} catch (error) {
|
|
@@ -171,33 +178,34 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
171
178
|
name: "onlyne_complete",
|
|
172
179
|
label: "Onlyne complete",
|
|
173
180
|
description:
|
|
174
|
-
"End
|
|
175
|
-
promptSnippet: "Finish the current
|
|
181
|
+
"End the current task with an explicit outcome: done (the work is finished), failed (it is provably impossible), cancelled (it was withdrawn), or blocked (something outside this session stops it). summary is the one-line result and details is the full one; files names the paths the result rests on. If the workspace requires a handoff before the task may end, the call is refused until that handoff has gone out.",
|
|
182
|
+
promptSnippet: "Finish the current task with an outcome and a one-line summary",
|
|
176
183
|
promptGuidelines: [
|
|
177
|
-
"Use onlyne_complete at the end of
|
|
178
|
-
"If the assignment is sent to you again while it is still open, the previous turn ended without a completion: finish the work and call onlyne_complete.",
|
|
179
|
-
"If onlyne_complete answers 'relay guard', the session still owes a downstream handoff: make it with onlyne_send and call onlyne_complete again. Close the session anyway only when the handoff is genuinely impossible, with force: true and a reason.",
|
|
184
|
+
"Use onlyne_complete at the end of the current task, naming the outcome and the result in one line.",
|
|
180
185
|
],
|
|
181
186
|
parameters: Type.Object({
|
|
182
|
-
outcome: Type.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
187
|
+
outcome: Type.String({ description: '"done", "failed", "cancelled", or "blocked"' }),
|
|
188
|
+
summary: Type.String({ description: "one-line result summary" }),
|
|
189
|
+
details: Type.Optional(Type.String({ description: "the full result, delivered as it stands" })),
|
|
190
|
+
files: Type.Optional(Type.Array(Type.String(), { description: "absolute paths of the files the result names" })),
|
|
186
191
|
}),
|
|
187
192
|
async execute(_toolCallId, params) {
|
|
188
193
|
if (!agent) throw new Error("onlyne: session is not connected");
|
|
189
194
|
// The exit is not a tool-result flag: pi 0.85.1 has no tool-result
|
|
190
195
|
// `terminate` handling. `agent.complete` asks the surface to shut the
|
|
191
|
-
// process down once the client has acknowledged the report. A
|
|
192
|
-
//
|
|
193
|
-
//
|
|
196
|
+
// process down once the client has acknowledged the report. A refusal
|
|
197
|
+
// from the client throws out of here as a tool error, so the model
|
|
198
|
+
// reads the host's own sentence.
|
|
194
199
|
const result = await agent.completeFromTool({
|
|
195
200
|
outcome: params.outcome,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
summary: params.summary,
|
|
202
|
+
details: params.details,
|
|
203
|
+
files: params.files,
|
|
199
204
|
});
|
|
200
|
-
|
|
205
|
+
// The outcome and nothing else: the ledger's head stays a display
|
|
206
|
+
// field (docs/v2-CONTRACT.md §3c), and the task's identity is not a
|
|
207
|
+
// fact the model is meant to hold.
|
|
208
|
+
return textResult(`reported ${result.outcome}`, { outcome: result.outcome });
|
|
201
209
|
},
|
|
202
210
|
}));
|
|
203
211
|
} catch (error) {
|
|
@@ -208,16 +216,14 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
208
216
|
name: "onlyne_handoff",
|
|
209
217
|
label: "Onlyne handoff",
|
|
210
218
|
description:
|
|
211
|
-
"Hand
|
|
212
|
-
promptSnippet: "Hand
|
|
219
|
+
"Hand the current task on to another role, which continues it. Call it when this task's work goes on to another role.",
|
|
220
|
+
promptSnippet: "Hand the current task on to another role",
|
|
213
221
|
promptGuidelines: [
|
|
214
|
-
"Use onlyne_handoff when
|
|
215
|
-
"Use onlyne_send with kind=\"task\" when a role should get work of its own: that child is hop 0 of a family this session starts.",
|
|
216
|
-
"Use onlyne_send with kind=\"note\" for free text to a role, which carries no task and no hop.",
|
|
222
|
+
"Use onlyne_handoff when this task's work goes on to another role; the receiving role continues it.",
|
|
217
223
|
],
|
|
218
224
|
parameters: Type.Object({
|
|
219
225
|
to: Type.String({ description: "target role name, e.g. builder" }),
|
|
220
|
-
text: Type.String({ description: "handoff text for the
|
|
226
|
+
text: Type.String({ description: "handoff text for the receiving role" }),
|
|
221
227
|
image: Type.Optional(Type.String({ description: "absolute path to a png/jpeg/gif/webp image to attach" })),
|
|
222
228
|
}),
|
|
223
229
|
async execute(_toolCallId, params) {
|
|
@@ -227,7 +233,10 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
227
233
|
text: params.text,
|
|
228
234
|
imagePath: params.image ?? null,
|
|
229
235
|
});
|
|
230
|
-
|
|
236
|
+
// The recipient and nothing else: the child's id and the hop are the
|
|
237
|
+
// host's bookkeeping, and a result naming them would teach the model
|
|
238
|
+
// to read itself as one node of a numbered chain.
|
|
239
|
+
return textResult(`handed on to ${result.to}`, { to: result.to });
|
|
231
240
|
},
|
|
232
241
|
}));
|
|
233
242
|
} catch (error) {
|
|
@@ -270,21 +279,18 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
270
279
|
log(`disabled by ${config.path}`);
|
|
271
280
|
return;
|
|
272
281
|
}
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const origin = relay.source === "env" ? "the client's environment" : relay.path;
|
|
283
|
-
log(
|
|
284
|
-
`relay guard from ${origin}: required=${JSON.stringify(relay.required)} count=${relay.count ?? "-"}`,
|
|
285
|
-
);
|
|
282
|
+
// The path the client injected, or the client the runtime directory's
|
|
283
|
+
// registration files name for this workspace (socket.mjs). A session with
|
|
284
|
+
// neither has no socket to dial, and saying so is the whole answer: this
|
|
285
|
+
// stays a plain pi session instead of retrying a path nothing serves.
|
|
286
|
+
let socketPath: string | null = null;
|
|
287
|
+
try {
|
|
288
|
+
socketPath = resolveSocketPath(env, ctx.cwd);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
log(`socket unresolved: ${error instanceof Error ? error.message : String(error)}`);
|
|
286
291
|
}
|
|
287
|
-
|
|
292
|
+
if (socketPath === null) return;
|
|
293
|
+
surface = createSurface({ pi, log, context: () => context, sessionId: identity.sessionId });
|
|
288
294
|
agent = new OnlyneAgent({
|
|
289
295
|
socketPath,
|
|
290
296
|
cwd: ctx.cwd,
|
|
@@ -292,8 +298,6 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
292
298
|
sessionId: identity.sessionId,
|
|
293
299
|
taskId: identity.taskId,
|
|
294
300
|
surface,
|
|
295
|
-
relay,
|
|
296
|
-
idleReminders: config.idleReminders,
|
|
297
301
|
log,
|
|
298
302
|
});
|
|
299
303
|
log(`session ${identity.sessionId} role=${identity.role} socket=${socketPath}`);
|
|
@@ -307,6 +311,15 @@ export default function onlyne(pi: ExtensionAPI) {
|
|
|
307
311
|
if (config.autoStart) agent.start();
|
|
308
312
|
});
|
|
309
313
|
|
|
314
|
+
// The role prose is instruction-layer text: `before_agent_start` hands the
|
|
315
|
+
// handler the prompt options the run is about to render, and a section written
|
|
316
|
+
// there is part of the system prompt rather than one more message the model has
|
|
317
|
+
// to read as an utterance. Outside an onlyne session there is no surface and
|
|
318
|
+
// nothing to add.
|
|
319
|
+
pi.on("before_agent_start", async (event) => {
|
|
320
|
+
surface?.applyRoleProse(event);
|
|
321
|
+
});
|
|
322
|
+
|
|
310
323
|
pi.on("turn_start", async () => {
|
|
311
324
|
agent?.onTurnStart();
|
|
312
325
|
});
|
package/src/pi-surface.mjs
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// through the pi extension API, with a probe for each optional member so an
|
|
3
3
|
// older pi degrades instead of throwing.
|
|
4
4
|
//
|
|
5
|
-
// Probed members (measured against pi 0.
|
|
5
|
+
// Probed members (measured against pi 0.87.1):
|
|
6
6
|
// wakeUser pi.sendUserMessage(content, { deliverAs: "followUp" })
|
|
7
|
-
//
|
|
7
|
+
// roleProse pi.on("before_agent_start") -> event.systemPromptOptions.sections
|
|
8
|
+
// (index.ts owns the subscription; this module holds the prose and
|
|
9
|
+
// writes the section it becomes)
|
|
8
10
|
// customEntry pi.appendEntry(customType, data)
|
|
9
11
|
// widget ctx.ui.setWidget("onlyne", lines) / ctx.ui.setWidget("onlyne", undefined)
|
|
10
12
|
// status ctx.ui.setStatus("onlyne", text)
|
|
@@ -13,15 +15,25 @@
|
|
|
13
15
|
// pending ctx.hasPendingMessages()
|
|
14
16
|
// toolNames pi.getAllTools() (background-work.mjs)
|
|
15
17
|
// eventBus pi.events (background-work.mjs)
|
|
18
|
+
// subagentRegistry ~/.pi/subagents/missions (background-subagents.mjs)
|
|
16
19
|
// registerTool / registerCommand are probed by index.ts itself.
|
|
17
20
|
|
|
18
21
|
import { WIDGET_KEY } from "./activity.mjs";
|
|
22
|
+
import { createSubagentProbe } from "./background-subagents.mjs";
|
|
19
23
|
import { createBackgroundProbe } from "./background-work.mjs";
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
26
|
+
* The system-prompt section the role prose occupies. pi wraps a section in a tag
|
|
27
|
+
* of the same name and records it under that name in the transcript, which is
|
|
28
|
+
* also where the live case reads the prose back from
|
|
29
|
+
* (`crates/onlyne-testkit/e2e/pi-live.sh`), so the name is stable.
|
|
23
30
|
*/
|
|
24
|
-
export
|
|
31
|
+
export const PROSE_SECTION = "onlyne-role-prose";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ pi: any, log: (line: string) => void, context: () => any, sessionId?: string | null }} options
|
|
35
|
+
*/
|
|
36
|
+
export function createSurface({ pi, log, context, sessionId = null }) {
|
|
25
37
|
const has = (value) => typeof value === "function";
|
|
26
38
|
const ctx = () => {
|
|
27
39
|
try {
|
|
@@ -55,9 +67,33 @@ export function createSurface({ pi, log, context }) {
|
|
|
55
67
|
log,
|
|
56
68
|
});
|
|
57
69
|
|
|
70
|
+
// The second family of off-loop work: `Agent` calls, which return a handle and
|
|
71
|
+
// leave a subagent running with no tool event to bracket it. Scoped to this
|
|
72
|
+
// session's own missions when the plugin knows its id.
|
|
73
|
+
const subagents = createSubagentProbe({
|
|
74
|
+
getSessionId: () => sessionId,
|
|
75
|
+
log,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Live work in either extension holds the turn open. Both probes fail safe on
|
|
80
|
+
* their own, and a throw from either is caught here, so a broken probe still
|
|
81
|
+
* resolves to "not running" and the session proceeds.
|
|
82
|
+
* @returns {Promise<boolean>}
|
|
83
|
+
*/
|
|
84
|
+
const backgroundRunning = async () => {
|
|
85
|
+
try {
|
|
86
|
+
const answers = await Promise.all([background.running(), subagents.running()]);
|
|
87
|
+
return answers.some(Boolean);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
log(`background work probe failed: ${message}`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
58
95
|
const available = {
|
|
59
96
|
wakeUser: has(pi.sendUserMessage),
|
|
60
|
-
proseContext: has(pi.sendMessage),
|
|
61
97
|
customEntry: has(pi.appendEntry),
|
|
62
98
|
widget: has(ctx()?.ui?.setWidget),
|
|
63
99
|
status: true,
|
|
@@ -83,19 +119,35 @@ export function createSurface({ pi, log, context }) {
|
|
|
83
119
|
}
|
|
84
120
|
};
|
|
85
121
|
|
|
86
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* One pi user message; images ride along as pi image content parts.
|
|
124
|
+
*
|
|
125
|
+
* pi reads an image part as the flat `ImageContent` of its message types —
|
|
126
|
+
* `data` plus `mimeType` — and normalizes every part before the message is
|
|
127
|
+
* built, so a part missing either string stops the whole delivery inside pi.
|
|
128
|
+
* pi reports that failure in its own pane and hands nothing back to this
|
|
129
|
+
* plugin, so an unusable part is dropped here and the assignment still
|
|
130
|
+
* travels: the delivery text already names the path the client wrote.
|
|
131
|
+
*/
|
|
87
132
|
const wakeUser = (text, parts = []) => {
|
|
88
133
|
if (!available.wakeUser) {
|
|
89
134
|
log("pi has no sendUserMessage; the assignment reached the session log only");
|
|
90
135
|
return false;
|
|
91
136
|
}
|
|
92
|
-
const
|
|
137
|
+
const images = parts.filter(
|
|
138
|
+
(part) => typeof part?.data === "string" && typeof part?.mime === "string" && part.mime,
|
|
139
|
+
);
|
|
140
|
+
if (images.length !== parts.length) {
|
|
141
|
+
log(`attachment carried no base64 data or no media type; ${parts.length - images.length} dropped, the task text still went`);
|
|
142
|
+
}
|
|
143
|
+
const content = images.length === 0
|
|
93
144
|
? text
|
|
94
145
|
: [
|
|
95
146
|
{ type: "text", text },
|
|
96
|
-
...
|
|
147
|
+
...images.map((part) => ({
|
|
97
148
|
type: "image",
|
|
98
|
-
|
|
149
|
+
data: part.data,
|
|
150
|
+
mimeType: part.mime,
|
|
99
151
|
})),
|
|
100
152
|
];
|
|
101
153
|
try {
|
|
@@ -114,26 +166,53 @@ export function createSurface({ pi, log, context }) {
|
|
|
114
166
|
};
|
|
115
167
|
|
|
116
168
|
/**
|
|
117
|
-
* The role prose
|
|
118
|
-
*
|
|
169
|
+
* The role prose this session was handed, held for the instruction layer.
|
|
170
|
+
*
|
|
171
|
+
* The client rendered it and this module adds nothing to it: no prefix, no
|
|
172
|
+
* label, no formatting. It reaches the model as a system-prompt section and
|
|
173
|
+
* never as a conversation message: a message would file the spec's prose in
|
|
174
|
+
* the transcript's message stream beside the delivery the model was asked to
|
|
175
|
+
* act on, and the model would read both as the same kind of thing.
|
|
119
176
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
let roleProseText = "";
|
|
178
|
+
|
|
179
|
+
/** One line for a pi without sectioned prompts, not one per run. */
|
|
180
|
+
let warnedNoSections = false;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Hand the role prose over once.
|
|
184
|
+
* @param {string} text
|
|
185
|
+
* @returns {boolean} whether there is prose to carry
|
|
186
|
+
*/
|
|
187
|
+
const roleProse = (text) => {
|
|
188
|
+
roleProseText = typeof text === "string" ? text : "";
|
|
189
|
+
return roleProseText.length > 0;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Put that prose into the run that is starting: one section of pi's system
|
|
194
|
+
* prompt, whose value is the client's bytes exactly.
|
|
195
|
+
*
|
|
196
|
+
* `before_agent_start` is pi's only instruction-layer point, and it hands the
|
|
197
|
+
* handler the prompt options it is about to render (`prompt-customizer.ts` in
|
|
198
|
+
* pi's own examples does this). pi rebuilds those options for every run, so the
|
|
199
|
+
* section is written again rather than once — the write is idempotent, and the
|
|
200
|
+
* first run records it in the transcript's system message.
|
|
201
|
+
* @param {{ systemPromptOptions?: { sections?: Record<string, string> } } | null} event
|
|
202
|
+
* @returns {boolean} whether a section was written
|
|
203
|
+
*/
|
|
204
|
+
const applyRoleProse = (event) => {
|
|
205
|
+
if (roleProseText.length === 0) return false;
|
|
206
|
+
const sections = event?.systemPromptOptions?.sections;
|
|
207
|
+
if (!sections) {
|
|
208
|
+
if (!warnedNoSections) {
|
|
209
|
+
warnedNoSections = true;
|
|
210
|
+
log("before_agent_start carries no prompt sections; the role prose reached no instruction layer");
|
|
211
|
+
}
|
|
135
212
|
return false;
|
|
136
213
|
}
|
|
214
|
+
sections[PROSE_SECTION] = roleProseText;
|
|
215
|
+
return true;
|
|
137
216
|
};
|
|
138
217
|
|
|
139
218
|
const customEntry = (customType, data) => {
|
|
@@ -151,7 +230,8 @@ export function createSurface({ pi, log, context }) {
|
|
|
151
230
|
return {
|
|
152
231
|
available,
|
|
153
232
|
wakeUser,
|
|
154
|
-
|
|
233
|
+
roleProse,
|
|
234
|
+
applyRoleProse,
|
|
155
235
|
customEntry,
|
|
156
236
|
widget,
|
|
157
237
|
status,
|
|
@@ -169,12 +249,12 @@ export function createSurface({ pi, log, context }) {
|
|
|
169
249
|
},
|
|
170
250
|
/**
|
|
171
251
|
* The phase rule in one place: idle means waiting for user input, and a
|
|
172
|
-
* background
|
|
173
|
-
* even while pi itself waits.
|
|
252
|
+
* background extension holding live work — a `bg_*` task or a subagent
|
|
253
|
+
* mission — keeps the session running even while pi itself waits.
|
|
174
254
|
*/
|
|
175
255
|
async waitingForInput() {
|
|
176
256
|
if (!piWaitsForInput()) return false;
|
|
177
|
-
return !(await
|
|
257
|
+
return !(await backgroundRunning());
|
|
178
258
|
},
|
|
179
259
|
closeBackground() {
|
|
180
260
|
background.close();
|