@tt-a1i/openpi 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +295 -389
- package/SETUP.md +24 -22
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/assets/readme-hero-mobile.svg +2 -2
- package/assets/readme-hero.svg +10 -10
- package/extensions/ask-user/handoff.ts +5 -1
- package/extensions/ask-user/index.ts +44 -0
- package/extensions/background-terminals/index.ts +118 -29
- package/extensions/background-terminals/src/domain.ts +5 -1
- package/extensions/background-terminals/src/manager.ts +2 -1
- package/extensions/background-terminals/src/prompt.ts +35 -0
- package/extensions/background-terminals/src/result-delivery.ts +76 -3
- package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
- package/extensions/capabilities/index.ts +198 -0
- package/extensions/context-pivot/index.ts +21 -0
- package/extensions/cron/index.ts +42 -15
- package/extensions/execution-convergence/active-evidence.ts +129 -0
- package/extensions/execution-convergence/index.ts +442 -0
- package/extensions/execution-convergence/workspace-provenance.ts +338 -0
- package/extensions/file-search/index.ts +8 -1
- package/extensions/file-search/src/binaries.ts +2 -1
- package/extensions/git-info/src/runtime.ts +1 -1
- package/extensions/goal/controller.ts +2 -1
- package/extensions/goal/index.ts +20 -1
- package/extensions/plan-mode/index.ts +12 -0
- package/extensions/setup/index.ts +241 -45
- package/extensions/setup/intercom-fs-helper.cjs +130 -0
- package/extensions/setup/intercom.ts +603 -0
- package/extensions/shared/child-session.ts +42 -5
- package/extensions/shared/setup-config.ts +27 -1
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +16 -1
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +1 -1
- package/extensions/tasks/index.ts +39 -12
- package/extensions/ui-customization/footer.ts +6 -1
- package/extensions/workflows/artifacts.ts +6 -1
- package/extensions/workflows/dashboard.ts +138 -27
- package/extensions/workflows/graph-projection.ts +240 -0
- package/extensions/workflows/handoff.ts +194 -0
- package/extensions/workflows/index.ts +258 -56
- package/extensions/workflows/invocation-ledger.ts +368 -0
- package/extensions/workflows/model.ts +57 -1
- package/extensions/workflows/operator.ts +131 -0
- package/extensions/workflows/prompt.ts +10 -38
- package/extensions/workflows/replay-safety.ts +9 -8
- package/extensions/workflows/runner.ts +10 -2
- package/extensions/workflows/sandbox.ts +5 -0
- package/package.json +15 -15
- package/skills/subagents/SKILL.md +6 -0
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
type Origin = "baseline" | "session_created";
|
|
5
|
+
|
|
6
|
+
interface PendingEffects {
|
|
7
|
+
creations: Array<{
|
|
8
|
+
path: string;
|
|
9
|
+
existed: boolean;
|
|
10
|
+
observeOnCommandError: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
removals: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Options {
|
|
16
|
+
confirmDelete: (paths: readonly string[]) => Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface BashAttempt {
|
|
20
|
+
id: string;
|
|
21
|
+
command: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WriteAttempt {
|
|
26
|
+
id: string;
|
|
27
|
+
path: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ShellInspection {
|
|
32
|
+
creations: string[];
|
|
33
|
+
removals: string[];
|
|
34
|
+
opaqueDestructiveCommand: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DYNAMIC_PATH = /[*?[$`]/u;
|
|
38
|
+
|
|
39
|
+
function splitShellSegments(command: string) {
|
|
40
|
+
const segments: string[] = [];
|
|
41
|
+
let current = "";
|
|
42
|
+
let quote: "'" | '"' | undefined;
|
|
43
|
+
let escaped = false;
|
|
44
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
45
|
+
const char = command[index] ?? "";
|
|
46
|
+
const next = command[index + 1] ?? "";
|
|
47
|
+
if (escaped) {
|
|
48
|
+
current += char;
|
|
49
|
+
escaped = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (char === "\\" && quote !== "'") {
|
|
53
|
+
current += char;
|
|
54
|
+
escaped = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (quote) {
|
|
58
|
+
current += char;
|
|
59
|
+
if (char === quote) quote = undefined;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === "'" || char === '"') {
|
|
63
|
+
quote = char;
|
|
64
|
+
current += char;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
char === "\n" ||
|
|
69
|
+
char === ";" ||
|
|
70
|
+
(char === "&" && next === "&") ||
|
|
71
|
+
(char === "|" && next === "|")
|
|
72
|
+
) {
|
|
73
|
+
if (current.trim()) segments.push(current.trim());
|
|
74
|
+
current = "";
|
|
75
|
+
if ((char === "&" || char === "|") && next === char) index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
current += char;
|
|
79
|
+
}
|
|
80
|
+
if (current.trim()) segments.push(current.trim());
|
|
81
|
+
return segments;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function shellTokens(segment: string) {
|
|
85
|
+
const tokens: string[] = [];
|
|
86
|
+
let current = "";
|
|
87
|
+
let quote: "'" | '"' | undefined;
|
|
88
|
+
let escaped = false;
|
|
89
|
+
const push = () => {
|
|
90
|
+
if (!current) return;
|
|
91
|
+
tokens.push(current);
|
|
92
|
+
current = "";
|
|
93
|
+
};
|
|
94
|
+
for (const char of segment) {
|
|
95
|
+
if (escaped) {
|
|
96
|
+
current += char;
|
|
97
|
+
escaped = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "\\" && quote !== "'") {
|
|
101
|
+
escaped = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (quote) {
|
|
105
|
+
if (char === quote) quote = undefined;
|
|
106
|
+
else current += char;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "'" || char === '"') {
|
|
110
|
+
quote = char;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (/\s/u.test(char)) {
|
|
114
|
+
push();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === ">") {
|
|
118
|
+
push();
|
|
119
|
+
tokens.push(">");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
current += char;
|
|
123
|
+
}
|
|
124
|
+
push();
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function literalPath(value: string | undefined) {
|
|
129
|
+
if (!value || value === "--" || DYNAMIC_PATH.test(value)) return undefined;
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function inspectShell(command: string): ShellInspection {
|
|
134
|
+
const creations = new Set<string>();
|
|
135
|
+
const removals = new Set<string>();
|
|
136
|
+
let opaqueDestructiveCommand = false;
|
|
137
|
+
for (const segment of splitShellSegments(command)) {
|
|
138
|
+
const tokens = shellTokens(segment);
|
|
139
|
+
const nestedBash = tokens.findIndex(
|
|
140
|
+
(token, index) =>
|
|
141
|
+
token.split("/").at(-1) === "bash" && tokens[index + 1] === "-c",
|
|
142
|
+
);
|
|
143
|
+
if (nestedBash >= 0) {
|
|
144
|
+
const nestedCommand = tokens[nestedBash + 2];
|
|
145
|
+
if (nestedCommand) {
|
|
146
|
+
const nested: ShellInspection = inspectShell(nestedCommand);
|
|
147
|
+
for (const target of nested.creations) creations.add(target);
|
|
148
|
+
for (const target of nested.removals) removals.add(target);
|
|
149
|
+
opaqueDestructiveCommand ||= nested.opaqueDestructiveCommand;
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
154
|
+
if (tokens[index] !== ">") continue;
|
|
155
|
+
const target = literalPath(tokens[index + 1]);
|
|
156
|
+
if (target) creations.add(target);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const executable = tokens[0]?.split("/").at(-1);
|
|
160
|
+
if (executable === "mkdir") {
|
|
161
|
+
const targets: string[] = [];
|
|
162
|
+
let afterOptions = false;
|
|
163
|
+
let supported = true;
|
|
164
|
+
for (const token of tokens.slice(1)) {
|
|
165
|
+
if (!afterOptions && token === "--") {
|
|
166
|
+
afterOptions = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!afterOptions && (token === "-p" || token === "--parents")) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!afterOptions && token.startsWith("-")) {
|
|
173
|
+
supported = false;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const target = literalPath(token);
|
|
177
|
+
if (!target) {
|
|
178
|
+
supported = false;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
targets.push(target);
|
|
182
|
+
}
|
|
183
|
+
if (supported) {
|
|
184
|
+
for (const target of targets) creations.add(target);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (executable !== "rm") continue;
|
|
188
|
+
let sawLiteral = false;
|
|
189
|
+
let afterOptions = false;
|
|
190
|
+
for (const token of tokens.slice(1)) {
|
|
191
|
+
if (!afterOptions && token === "--") {
|
|
192
|
+
afterOptions = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!afterOptions && token.startsWith("-")) continue;
|
|
196
|
+
const target = literalPath(token);
|
|
197
|
+
if (!target) {
|
|
198
|
+
opaqueDestructiveCommand = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
sawLiteral = true;
|
|
202
|
+
removals.add(target);
|
|
203
|
+
}
|
|
204
|
+
if (!sawLiteral) opaqueDestructiveCommand = true;
|
|
205
|
+
}
|
|
206
|
+
if (/\brm\b/u.test(command) && removals.size === 0) {
|
|
207
|
+
opaqueDestructiveCommand = true;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
creations: [...creations],
|
|
211
|
+
removals: [...removals],
|
|
212
|
+
opaqueDestructiveCommand,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function containedPath(cwd: string, candidate: string) {
|
|
217
|
+
const absolute = path.resolve(cwd, candidate);
|
|
218
|
+
const relative = path.relative(cwd, absolute);
|
|
219
|
+
if (
|
|
220
|
+
relative === "" ||
|
|
221
|
+
relative.startsWith("..") ||
|
|
222
|
+
path.isAbsolute(relative)
|
|
223
|
+
) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
return { absolute, relative: relative.split(path.sep).join("/") };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function exists(candidate: string) {
|
|
230
|
+
try {
|
|
231
|
+
await lstat(candidate);
|
|
232
|
+
return true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createWorkspaceCleanupGuard(options: Options) {
|
|
242
|
+
const origins = new Map<string, Origin>();
|
|
243
|
+
const pending = new Map<string, PendingEffects>();
|
|
244
|
+
|
|
245
|
+
const prepareCreation = async (
|
|
246
|
+
cwd: string,
|
|
247
|
+
candidate: string,
|
|
248
|
+
observeOnCommandError: boolean,
|
|
249
|
+
) => {
|
|
250
|
+
const contained = containedPath(cwd, candidate);
|
|
251
|
+
if (!contained) return undefined;
|
|
252
|
+
const existed = await exists(contained.absolute);
|
|
253
|
+
if (existed && !origins.has(contained.absolute)) {
|
|
254
|
+
origins.set(contained.absolute, "baseline");
|
|
255
|
+
}
|
|
256
|
+
return { path: contained.absolute, existed, observeOnCommandError };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
async beforeWrite(attempt: WriteAttempt) {
|
|
261
|
+
const creation = await prepareCreation(attempt.cwd, attempt.path, false);
|
|
262
|
+
pending.set(attempt.id, {
|
|
263
|
+
creations: creation ? [creation] : [],
|
|
264
|
+
removals: [],
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async before(attempt: BashAttempt) {
|
|
269
|
+
const inspected = inspectShell(attempt.command);
|
|
270
|
+
const creations: PendingEffects["creations"] = [];
|
|
271
|
+
for (const candidate of inspected.creations) {
|
|
272
|
+
const creation = await prepareCreation(attempt.cwd, candidate, true);
|
|
273
|
+
if (creation) creations.push(creation);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const removals: string[] = [];
|
|
277
|
+
const protectedPaths: string[] = [];
|
|
278
|
+
for (const candidate of inspected.removals) {
|
|
279
|
+
const contained = containedPath(attempt.cwd, candidate);
|
|
280
|
+
if (!contained) continue;
|
|
281
|
+
const origin = origins.get(contained.absolute);
|
|
282
|
+
const present = await exists(contained.absolute);
|
|
283
|
+
if (present && !origin) origins.set(contained.absolute, "baseline");
|
|
284
|
+
if (present && origins.get(contained.absolute) !== "session_created") {
|
|
285
|
+
protectedPaths.push(contained.relative);
|
|
286
|
+
}
|
|
287
|
+
removals.push(contained.absolute);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
protectedPaths.length > 0 &&
|
|
292
|
+
!(await options.confirmDelete(protectedPaths))
|
|
293
|
+
) {
|
|
294
|
+
return {
|
|
295
|
+
kind: "block" as const,
|
|
296
|
+
protectedPaths,
|
|
297
|
+
reason: `Blocked cleanup: ${protectedPaths.join(", ")} existed before this agent changed it and is not proven session-created scratch. Retry the cleanup without that path, or obtain explicit user confirmation to delete it.`,
|
|
298
|
+
opaqueDestructiveCommand: inspected.opaqueDestructiveCommand,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
pending.set(attempt.id, { creations, removals });
|
|
303
|
+
return {
|
|
304
|
+
kind: "allow" as const,
|
|
305
|
+
opaqueDestructiveCommand: inspected.opaqueDestructiveCommand,
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async after(result: { id: string; isError: boolean }) {
|
|
310
|
+
const effects = pending.get(result.id);
|
|
311
|
+
pending.delete(result.id);
|
|
312
|
+
if (!effects) return;
|
|
313
|
+
for (const creation of effects.creations) {
|
|
314
|
+
if (
|
|
315
|
+
!creation.existed &&
|
|
316
|
+
(!result.isError || creation.observeOnCommandError) &&
|
|
317
|
+
!origins.has(creation.path) &&
|
|
318
|
+
(await exists(creation.path))
|
|
319
|
+
) {
|
|
320
|
+
origins.set(creation.path, "session_created");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const removed of effects.removals) {
|
|
324
|
+
if (
|
|
325
|
+
origins.get(removed) === "session_created" &&
|
|
326
|
+
!(await exists(removed))
|
|
327
|
+
) {
|
|
328
|
+
origins.delete(removed);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
reset() {
|
|
334
|
+
origins.clear();
|
|
335
|
+
pending.clear();
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* before executing, and report a clear error if it failed.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import type {
|
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
RG_TOOL_DESCRIPTION,
|
|
54
54
|
} from "./src/prompt.ts";
|
|
55
55
|
import { discardCapturedOutput, executeSearchProcess } from "./src/process.ts";
|
|
56
|
+
import {
|
|
57
|
+
OPENPI_TOOL_SURFACE,
|
|
58
|
+
patchOwnedTools,
|
|
59
|
+
} from "../shared/tool-surface.ts";
|
|
56
60
|
|
|
57
61
|
export function makeBinaryInitializers(
|
|
58
62
|
binDir: string,
|
|
@@ -137,6 +141,9 @@ export default function fileSearchTools(pi: ExtensionAPI) {
|
|
|
137
141
|
const initializers = makeBinaryInitializers(binDir, target, liveBinaryEnv);
|
|
138
142
|
|
|
139
143
|
pi.on("session_start", async (_event, ctx) => {
|
|
144
|
+
patchOwnedTools(pi, "fileSearch", {
|
|
145
|
+
enable: OPENPI_TOOL_SURFACE.fileSearch.entry,
|
|
146
|
+
});
|
|
140
147
|
const exit = await Effect.runPromiseExit(
|
|
141
148
|
Effect.gen(function* () {
|
|
142
149
|
const initialized = yield* Effect.all(
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* network. `liveBinaryEnv` is the real implementation.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
16
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
16
17
|
import { execFile } from "node:child_process";
|
|
17
18
|
import { tmpdir } from "node:os";
|
|
18
19
|
import { dirname, join } from "node:path";
|
|
@@ -68,7 +68,8 @@ export function countAssistantTokens(messages: readonly unknown[]) {
|
|
|
68
68
|
|
|
69
69
|
export function lastAssistantStopReason(messages: readonly unknown[]) {
|
|
70
70
|
return lastAssistantMessage(messages)?.stopReason as
|
|
71
|
-
|
|
71
|
+
| AssistantStopReason
|
|
72
|
+
| undefined;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export function isUsageLimitError(messages: readonly unknown[]) {
|
package/extensions/goal/index.ts
CHANGED
|
@@ -6,6 +6,10 @@ import type {
|
|
|
6
6
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
9
|
+
import {
|
|
10
|
+
OPENPI_TOOL_SURFACE,
|
|
11
|
+
patchOwnedTools,
|
|
12
|
+
} from "../shared/tool-surface.ts";
|
|
9
13
|
import { GOAL_CONTINUATION_TYPE, GoalController } from "./controller.ts";
|
|
10
14
|
import {
|
|
11
15
|
GOAL_LIMITS,
|
|
@@ -88,6 +92,14 @@ export function goalToolResponse(
|
|
|
88
92
|
|
|
89
93
|
export default function sessionGoal(pi: ExtensionAPI) {
|
|
90
94
|
const controller = new GoalController(pi);
|
|
95
|
+
const hideLifecycleTools = () =>
|
|
96
|
+
patchOwnedTools(pi, "goal", {
|
|
97
|
+
disable: OPENPI_TOOL_SURFACE.goal.deferred,
|
|
98
|
+
});
|
|
99
|
+
const showLifecycleTools = () =>
|
|
100
|
+
patchOwnedTools(pi, "goal", {
|
|
101
|
+
enable: OPENPI_TOOL_SURFACE.goal.deferred,
|
|
102
|
+
});
|
|
91
103
|
|
|
92
104
|
const updateUi = (ctx: ExtensionContext) => {
|
|
93
105
|
if (!ctx.hasUI) return;
|
|
@@ -130,6 +142,7 @@ export default function sessionGoal(pi: ExtensionAPI) {
|
|
|
130
142
|
controller.createId(),
|
|
131
143
|
);
|
|
132
144
|
controller.replace(goal);
|
|
145
|
+
showLifecycleTools();
|
|
133
146
|
const started = controller.kickoff(ctx);
|
|
134
147
|
return { goal: controller.snapshot() ?? goal, started };
|
|
135
148
|
};
|
|
@@ -325,9 +338,11 @@ export default function sessionGoal(pi: ExtensionAPI) {
|
|
|
325
338
|
// contradicts.
|
|
326
339
|
notify(ctx, goalUpdateMessage(controller.snapshot()!));
|
|
327
340
|
} else if (parsed.action === "clear") {
|
|
341
|
+
const cleared = controller.clear();
|
|
342
|
+
if (cleared) hideLifecycleTools();
|
|
328
343
|
notify(
|
|
329
344
|
ctx,
|
|
330
|
-
|
|
345
|
+
cleared
|
|
331
346
|
? "Goal cleared"
|
|
332
347
|
: "No goal to clear\nThis thread does not currently have a goal.",
|
|
333
348
|
);
|
|
@@ -387,6 +402,8 @@ export default function sessionGoal(pi: ExtensionAPI) {
|
|
|
387
402
|
|
|
388
403
|
pi.on("session_start", async (event, ctx) => {
|
|
389
404
|
controller.restore(ctx, event.reason === "fork");
|
|
405
|
+
if (controller.snapshot()) showLifecycleTools();
|
|
406
|
+
else hideLifecycleTools();
|
|
390
407
|
if (controller.problem()) {
|
|
391
408
|
notify(
|
|
392
409
|
ctx,
|
|
@@ -419,6 +436,8 @@ export default function sessionGoal(pi: ExtensionAPI) {
|
|
|
419
436
|
|
|
420
437
|
pi.on("session_tree", (_event, ctx) => {
|
|
421
438
|
controller.restore(ctx, true);
|
|
439
|
+
if (controller.snapshot()) showLifecycleTools();
|
|
440
|
+
else hideLifecycleTools();
|
|
422
441
|
updateUi(ctx);
|
|
423
442
|
});
|
|
424
443
|
|
|
@@ -36,6 +36,10 @@ import {
|
|
|
36
36
|
PLAN_MODE_CHANNEL,
|
|
37
37
|
type PlanModeState,
|
|
38
38
|
} from "../shared/plan-mode-state.ts";
|
|
39
|
+
import {
|
|
40
|
+
OPENPI_TOOL_SURFACE,
|
|
41
|
+
patchOwnedTools,
|
|
42
|
+
} from "../shared/tool-surface.ts";
|
|
39
43
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
40
44
|
import { planBashDecision } from "./bash-policy.ts";
|
|
41
45
|
|
|
@@ -231,6 +235,12 @@ export function planToolCallDecision(
|
|
|
231
235
|
export default function planMode(pi: ExtensionAPI) {
|
|
232
236
|
let planning = false;
|
|
233
237
|
let readyPlan: string | undefined;
|
|
238
|
+
const syncPlanTool = () =>
|
|
239
|
+
patchOwnedTools(pi, "plan", {
|
|
240
|
+
...(planning && !readyPlan
|
|
241
|
+
? { enable: OPENPI_TOOL_SURFACE.plan.deferred }
|
|
242
|
+
: { disable: OPENPI_TOOL_SURFACE.plan.deferred }),
|
|
243
|
+
});
|
|
234
244
|
|
|
235
245
|
/**
|
|
236
246
|
* Publish the stance and reflect it in the footer. Every place `planning`
|
|
@@ -242,6 +252,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
242
252
|
hasUI: boolean;
|
|
243
253
|
ui: { setStatus: (key: string, value?: string) => void };
|
|
244
254
|
}) => {
|
|
255
|
+
syncPlanTool();
|
|
245
256
|
pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
|
|
246
257
|
if (!ctx.hasUI) return;
|
|
247
258
|
ctx.ui.setStatus(
|
|
@@ -532,6 +543,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
532
543
|
pi.on("session_shutdown", () => {
|
|
533
544
|
planning = false;
|
|
534
545
|
readyPlan = undefined;
|
|
546
|
+
syncPlanTool();
|
|
535
547
|
// Broadcast without a ctx: subagents keeps its own copy of the stance, and
|
|
536
548
|
// leaving it armed would restrict children in whatever session comes next.
|
|
537
549
|
pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
|