@tt-a1i/openpi 0.5.0 → 0.6.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 +18 -10
- package/SETUP.md +8 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +25 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +44 -4
- package/extensions/subagents/src/backends/pi.ts +76 -5
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +5 -0
- package/extensions/subagents/src/prompt.ts +17 -3
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +8 -0
- package/extensions/workflows/index.ts +13 -0
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +6 -71
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +3 -2
- package/skills/subagents/SKILL.md +1 -0
- package/skills/workflows/REFERENCE.md +3 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -22,6 +22,9 @@ const UNITS: Record<string, number> = {
|
|
|
22
22
|
|
|
23
23
|
export const MIN_INTERVAL_MS = 30_000;
|
|
24
24
|
export const CRON_PROMPT_MAX_CHARS = 2_000;
|
|
25
|
+
export const CRON_MAX_JOBS = 64;
|
|
26
|
+
export const CRON_DELIVERY_MAX_JOBS = 16;
|
|
27
|
+
export const CRON_DELIVERY_MAX_BYTES = 48 * 1024;
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Parse a duration like `30s`, `5m`, `2h`. Deliberately a small duration
|
|
@@ -33,7 +36,8 @@ export function parseDuration(text: string): number | undefined {
|
|
|
33
36
|
if (!match) return undefined;
|
|
34
37
|
const value = Number(match[1]);
|
|
35
38
|
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
36
|
-
|
|
39
|
+
const durationMs = value * UNITS[match[2].toLowerCase()];
|
|
40
|
+
return Number.isSafeInteger(durationMs) ? durationMs : undefined;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface ParsedCronCommand {
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
export const CACHE_DIAGNOSTICS_CHANNEL = "model-info:cache-diagnostics";
|
|
5
|
+
|
|
6
|
+
export const CACHE_WARM_MINIMUM_TOKENS = 2_048;
|
|
7
|
+
|
|
8
|
+
export type CacheSemantics =
|
|
9
|
+
| "explicit-prefix"
|
|
10
|
+
| "implicit-best-effort"
|
|
11
|
+
| "unknown";
|
|
12
|
+
|
|
13
|
+
export type CacheObservationKind =
|
|
14
|
+
| "first-turn"
|
|
15
|
+
| "cold"
|
|
16
|
+
| "warm"
|
|
17
|
+
| "partial-hit"
|
|
18
|
+
| "miss-after-warm-prefix"
|
|
19
|
+
| "unknown";
|
|
20
|
+
|
|
21
|
+
export type CacheCorrelation =
|
|
22
|
+
| "model-change"
|
|
23
|
+
| "thinking-change"
|
|
24
|
+
| "tool-surface-change"
|
|
25
|
+
| "system-prompt-change"
|
|
26
|
+
| "compaction"
|
|
27
|
+
| "branch-change";
|
|
28
|
+
|
|
29
|
+
export interface CacheTurnIdentity {
|
|
30
|
+
provider: string;
|
|
31
|
+
modelId: string;
|
|
32
|
+
thinking: string;
|
|
33
|
+
toolSurfaceFingerprint: string;
|
|
34
|
+
systemPromptFingerprint: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CacheTurnObservation {
|
|
38
|
+
turnIndex: number;
|
|
39
|
+
provider: string;
|
|
40
|
+
semantics: CacheSemantics;
|
|
41
|
+
kind: CacheObservationKind;
|
|
42
|
+
usage: {
|
|
43
|
+
input: number;
|
|
44
|
+
cacheRead: number;
|
|
45
|
+
cacheWrite: number;
|
|
46
|
+
promptTokens: number;
|
|
47
|
+
};
|
|
48
|
+
previousCacheRead: number | null;
|
|
49
|
+
reprocessedTokens: number | null;
|
|
50
|
+
correlations: CacheCorrelation[];
|
|
51
|
+
evidence: "observation";
|
|
52
|
+
verifiedCause: null;
|
|
53
|
+
explanation: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type TurnSample = {
|
|
57
|
+
identity: CacheTurnIdentity;
|
|
58
|
+
usage: CacheTurnObservation["usage"];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const IMPLICIT_CACHE_PROVIDERS = new Set([
|
|
62
|
+
"azure-openai-responses",
|
|
63
|
+
"google",
|
|
64
|
+
"google-antigravity",
|
|
65
|
+
"google-gemini-cli",
|
|
66
|
+
"openai",
|
|
67
|
+
"openai-codex",
|
|
68
|
+
"openai-responses",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
export function cacheSemanticsForProvider(provider: string): CacheSemantics {
|
|
72
|
+
const normalized = provider.trim().toLowerCase();
|
|
73
|
+
if (normalized === "anthropic") return "explicit-prefix";
|
|
74
|
+
if (IMPLICIT_CACHE_PROVIDERS.has(normalized)) return "implicit-best-effort";
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function fingerprintCacheSurface(value: unknown) {
|
|
79
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function finiteUsage(value: number) {
|
|
83
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function promptUsage(usage: Usage): CacheTurnObservation["usage"] {
|
|
87
|
+
const input = finiteUsage(usage.input);
|
|
88
|
+
const cacheRead = finiteUsage(usage.cacheRead);
|
|
89
|
+
const cacheWrite = finiteUsage(usage.cacheWrite);
|
|
90
|
+
return {
|
|
91
|
+
input,
|
|
92
|
+
cacheRead,
|
|
93
|
+
cacheWrite,
|
|
94
|
+
promptTokens: input + cacheRead + cacheWrite,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function identityCorrelations(
|
|
99
|
+
previous: CacheTurnIdentity,
|
|
100
|
+
current: CacheTurnIdentity,
|
|
101
|
+
) {
|
|
102
|
+
const correlations: CacheCorrelation[] = [];
|
|
103
|
+
if (
|
|
104
|
+
previous.provider !== current.provider ||
|
|
105
|
+
previous.modelId !== current.modelId
|
|
106
|
+
) {
|
|
107
|
+
correlations.push("model-change");
|
|
108
|
+
}
|
|
109
|
+
if (previous.thinking !== current.thinking) {
|
|
110
|
+
correlations.push("thinking-change");
|
|
111
|
+
}
|
|
112
|
+
if (previous.toolSurfaceFingerprint !== current.toolSurfaceFingerprint) {
|
|
113
|
+
correlations.push("tool-surface-change");
|
|
114
|
+
}
|
|
115
|
+
if (previous.systemPromptFingerprint !== current.systemPromptFingerprint) {
|
|
116
|
+
correlations.push("system-prompt-change");
|
|
117
|
+
}
|
|
118
|
+
return correlations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function classify(
|
|
122
|
+
semantics: CacheSemantics,
|
|
123
|
+
current: CacheTurnObservation["usage"],
|
|
124
|
+
previous: TurnSample | undefined,
|
|
125
|
+
): Pick<CacheTurnObservation, "kind" | "reprocessedTokens" | "explanation"> {
|
|
126
|
+
if (!previous) {
|
|
127
|
+
return {
|
|
128
|
+
kind: "first-turn",
|
|
129
|
+
reprocessedTokens: null,
|
|
130
|
+
explanation:
|
|
131
|
+
"No prior turn exists, so cache continuity cannot be inferred.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const previousWarm = previous.usage.cacheRead >= CACHE_WARM_MINIMUM_TOKENS;
|
|
136
|
+
if (current.cacheRead > 0) {
|
|
137
|
+
return {
|
|
138
|
+
kind:
|
|
139
|
+
current.cacheRead < previous.usage.cacheRead ? "partial-hit" : "warm",
|
|
140
|
+
reprocessedTokens: null,
|
|
141
|
+
explanation:
|
|
142
|
+
current.cacheRead < previous.usage.cacheRead
|
|
143
|
+
? "The provider reported a smaller cache read than on the prior turn."
|
|
144
|
+
: "The provider reported cached prompt tokens on this turn.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!previousWarm) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "cold",
|
|
150
|
+
reprocessedTokens: null,
|
|
151
|
+
explanation:
|
|
152
|
+
"The prior turn had no sufficiently warm prefix, so this cold turn is not an invalidation signal.",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (semantics !== "explicit-prefix") {
|
|
156
|
+
return {
|
|
157
|
+
kind: "unknown",
|
|
158
|
+
reprocessedTokens: null,
|
|
159
|
+
explanation:
|
|
160
|
+
semantics === "implicit-best-effort"
|
|
161
|
+
? "A warm-to-cold transition was observed, but this provider exposes only best-effort cache usage."
|
|
162
|
+
: "A warm-to-cold transition was observed, but the provider cache contract is unknown.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
kind: "miss-after-warm-prefix",
|
|
167
|
+
reprocessedTokens: current.input,
|
|
168
|
+
explanation:
|
|
169
|
+
"An explicit-prefix provider reported a warm prior turn and zero cache read now; local boundaries are correlations, not verified causes.",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function createCacheDiagnosticsTracker() {
|
|
174
|
+
let previous: TurnSample | undefined;
|
|
175
|
+
let pendingCorrelations = new Set<CacheCorrelation>();
|
|
176
|
+
|
|
177
|
+
const reset = () => {
|
|
178
|
+
previous = undefined;
|
|
179
|
+
pendingCorrelations = new Set();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const mark = (correlation: CacheCorrelation) => {
|
|
183
|
+
pendingCorrelations.add(correlation);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const observe = (options: {
|
|
187
|
+
turnIndex: number;
|
|
188
|
+
identity: CacheTurnIdentity;
|
|
189
|
+
usage: Usage;
|
|
190
|
+
}) => {
|
|
191
|
+
const usage = promptUsage(options.usage);
|
|
192
|
+
const semantics = cacheSemanticsForProvider(options.identity.provider);
|
|
193
|
+
const classification = classify(semantics, usage, previous);
|
|
194
|
+
const correlations = previous
|
|
195
|
+
? identityCorrelations(previous.identity, options.identity)
|
|
196
|
+
: [];
|
|
197
|
+
for (const pending of pendingCorrelations) correlations.push(pending);
|
|
198
|
+
const uniqueCorrelations = [...new Set(correlations)];
|
|
199
|
+
|
|
200
|
+
const observation: CacheTurnObservation = {
|
|
201
|
+
turnIndex: options.turnIndex,
|
|
202
|
+
provider: options.identity.provider,
|
|
203
|
+
semantics,
|
|
204
|
+
kind: classification.kind,
|
|
205
|
+
usage,
|
|
206
|
+
previousCacheRead: previous?.usage.cacheRead ?? null,
|
|
207
|
+
reprocessedTokens: classification.reprocessedTokens,
|
|
208
|
+
correlations: uniqueCorrelations,
|
|
209
|
+
evidence: "observation",
|
|
210
|
+
verifiedCause: null,
|
|
211
|
+
explanation: classification.explanation,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
previous = { identity: options.identity, usage };
|
|
215
|
+
pendingCorrelations.clear();
|
|
216
|
+
return observation;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return { mark, observe, reset };
|
|
220
|
+
}
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
REFRESH_CHANNEL,
|
|
9
9
|
} from "../shared/dashboard-state.ts";
|
|
10
10
|
import { createSessionMetricsTracker } from "./session-metrics.ts";
|
|
11
|
+
import {
|
|
12
|
+
CACHE_DIAGNOSTICS_CHANNEL,
|
|
13
|
+
createCacheDiagnosticsTracker,
|
|
14
|
+
fingerprintCacheSurface,
|
|
15
|
+
type CacheTurnIdentity,
|
|
16
|
+
} from "./cache-diagnostics.ts";
|
|
11
17
|
|
|
12
18
|
const CHARS_PER_ESTIMATED_TOKEN = 4;
|
|
13
19
|
const LIVE_UPDATE_INTERVAL_MS = 200;
|
|
@@ -29,6 +35,8 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
29
35
|
let lastLiveUpdate = 0;
|
|
30
36
|
let currentContext: ExtensionContext | undefined;
|
|
31
37
|
const sessionMetrics = createSessionMetricsTracker();
|
|
38
|
+
const cacheDiagnostics = createCacheDiagnosticsTracker();
|
|
39
|
+
let cacheIdentity: CacheTurnIdentity | undefined;
|
|
32
40
|
|
|
33
41
|
const publish = () => pi.events.emit(MODEL_INFO_CHANNEL, { ...state });
|
|
34
42
|
|
|
@@ -74,11 +82,14 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
74
82
|
runContentStreamMs = 0;
|
|
75
83
|
state = { ...state, tokensPerSecond: null, generating: false };
|
|
76
84
|
sessionMetrics.reset();
|
|
85
|
+
cacheDiagnostics.reset();
|
|
86
|
+
cacheIdentity = undefined;
|
|
77
87
|
syncSessionMetrics(ctx);
|
|
78
88
|
refresh(ctx);
|
|
79
89
|
});
|
|
80
90
|
|
|
81
91
|
pi.on("model_select", (event, ctx) => {
|
|
92
|
+
cacheDiagnostics.mark("model-change");
|
|
82
93
|
state = {
|
|
83
94
|
...state,
|
|
84
95
|
provider: event.model.provider,
|
|
@@ -91,6 +102,7 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
91
102
|
});
|
|
92
103
|
|
|
93
104
|
pi.on("thinking_level_select", (event) => {
|
|
105
|
+
cacheDiagnostics.mark("thinking-change");
|
|
94
106
|
state = { ...state, thinking: event.level };
|
|
95
107
|
publish();
|
|
96
108
|
});
|
|
@@ -103,6 +115,17 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
103
115
|
refresh(ctx);
|
|
104
116
|
});
|
|
105
117
|
|
|
118
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
119
|
+
const selectedTools = event.systemPromptOptions.selectedTools ?? [];
|
|
120
|
+
cacheIdentity = {
|
|
121
|
+
provider: ctx.model?.provider ?? "",
|
|
122
|
+
modelId: ctx.model?.id ?? "no-model",
|
|
123
|
+
thinking: ctx.model?.reasoning ? pi.getThinkingLevel() : "off",
|
|
124
|
+
toolSurfaceFingerprint: fingerprintCacheSurface(selectedTools),
|
|
125
|
+
systemPromptFingerprint: fingerprintCacheSurface(event.systemPrompt),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
106
129
|
pi.on("message_start", (event) => {
|
|
107
130
|
if (event.message.role === "assistant") resetMessageTracking();
|
|
108
131
|
});
|
|
@@ -191,20 +214,39 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
191
214
|
refresh(ctx);
|
|
192
215
|
});
|
|
193
216
|
|
|
194
|
-
pi.on("turn_end", (
|
|
217
|
+
pi.on("turn_end", (event, ctx) => {
|
|
195
218
|
syncSessionMetrics(ctx);
|
|
196
219
|
refresh(ctx);
|
|
220
|
+
// Failed/cancelled responses may carry placeholder zero usage. They do
|
|
221
|
+
// not establish a cache observation or replace the last valid baseline.
|
|
222
|
+
if (
|
|
223
|
+
event.message?.role === "assistant" &&
|
|
224
|
+
event.message.stopReason !== "error" &&
|
|
225
|
+
event.message.stopReason !== "aborted" &&
|
|
226
|
+
cacheIdentity
|
|
227
|
+
) {
|
|
228
|
+
pi.events.emit(
|
|
229
|
+
CACHE_DIAGNOSTICS_CHANNEL,
|
|
230
|
+
cacheDiagnostics.observe({
|
|
231
|
+
turnIndex: event.turnIndex,
|
|
232
|
+
identity: cacheIdentity,
|
|
233
|
+
usage: event.message.usage,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
197
237
|
});
|
|
198
238
|
|
|
199
239
|
// Compaction and branch moves rewrite history, so the cached percentage is
|
|
200
240
|
// stale the moment they land. Pi reports unknown occupancy until the next
|
|
201
241
|
// assistant reply, which is the honest state to show.
|
|
202
242
|
pi.on("session_compact", (_event, ctx) => {
|
|
243
|
+
cacheDiagnostics.mark("compaction");
|
|
203
244
|
syncSessionMetrics(ctx);
|
|
204
245
|
refresh(ctx);
|
|
205
246
|
});
|
|
206
247
|
|
|
207
248
|
pi.on("session_tree", (_event, ctx) => {
|
|
249
|
+
cacheDiagnostics.mark("branch-change");
|
|
208
250
|
syncSessionMetrics(ctx);
|
|
209
251
|
refresh(ctx);
|
|
210
252
|
});
|
|
@@ -218,5 +260,7 @@ export default function modelInfo(pi: ExtensionAPI) {
|
|
|
218
260
|
stopRefreshListener();
|
|
219
261
|
currentContext = undefined;
|
|
220
262
|
sessionMetrics.reset();
|
|
263
|
+
cacheDiagnostics.reset();
|
|
264
|
+
cacheIdentity = undefined;
|
|
221
265
|
});
|
|
222
266
|
}
|
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
* may predate the plan and still hold the full tool set.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
import {
|
|
30
|
+
getMarkdownTheme,
|
|
31
|
+
keyHint,
|
|
32
|
+
type ExtensionAPI,
|
|
33
|
+
type ExtensionCommandContext,
|
|
34
|
+
type ExtensionContext,
|
|
33
35
|
} from "@earendil-works/pi-coding-agent";
|
|
36
|
+
import { Markdown, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
34
37
|
import { Type } from "typebox";
|
|
35
38
|
import {
|
|
36
39
|
PLAN_MODE_CHANNEL,
|
|
@@ -46,6 +49,8 @@ import { planBashDecision } from "./bash-policy.ts";
|
|
|
46
49
|
|
|
47
50
|
export const MAX_READY_PLAN_CHARS = 50_000;
|
|
48
51
|
export const MAX_READY_PLAN_UTF8_BYTES = 48_000;
|
|
52
|
+
/** Preview lines shown in the collapsed plan_ready result. */
|
|
53
|
+
const PLAN_PREVIEW_LINES = 10;
|
|
49
54
|
export const PLAN_MODE_STATE_ENTRY = "my-pi-setup-plan-mode-state";
|
|
50
55
|
|
|
51
56
|
export type PersistedPlanModeState =
|
|
@@ -450,6 +455,72 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
450
455
|
terminate: true,
|
|
451
456
|
};
|
|
452
457
|
},
|
|
458
|
+
/**
|
|
459
|
+
* Render a completed plan for the TUI. Collapsed shows the line count
|
|
460
|
+
* plus a bounded preview (PLAN_PREVIEW_LINES) with an expand hint;
|
|
461
|
+
* expanded renders the full plan as Markdown. Pi only reflects the
|
|
462
|
+
* expanded flag — it never truncates custom renderer output, so this
|
|
463
|
+
* renderer owns the collapsed/expanded contract.
|
|
464
|
+
*/
|
|
465
|
+
renderResult(result, { expanded }, theme) {
|
|
466
|
+
const plan = (result.details as { plan?: string } | undefined)?.plan;
|
|
467
|
+
if (!plan) {
|
|
468
|
+
// Failed invocations (not in plan mode, empty plan, size cap, abort)
|
|
469
|
+
// produce {content:[{text:reason}], details:{}} from agent-loop's
|
|
470
|
+
// createErrorToolResult; surface the real reason instead of a
|
|
471
|
+
// placeholder, mirroring subagent_spawn's fallback.
|
|
472
|
+
const first = result.content?.[0];
|
|
473
|
+
return new Text(
|
|
474
|
+
first?.type === "text"
|
|
475
|
+
? first.text
|
|
476
|
+
: theme.fg("muted", "(no plan content)"),
|
|
477
|
+
0,
|
|
478
|
+
0,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (!expanded) {
|
|
482
|
+
// Bound the collapsed preview by rendered rows, not source lines: a
|
|
483
|
+
// single long source line wraps into many terminal rows at narrow
|
|
484
|
+
// widths, so render the body at the caller's width, keep the first
|
|
485
|
+
// PLAN_PREVIEW_LINES rows, and truncate every row to the viewport
|
|
486
|
+
// width (same contract as renderWaitResult's fixedRows).
|
|
487
|
+
const body = new Text(
|
|
488
|
+
plan
|
|
489
|
+
.split("\n")
|
|
490
|
+
.map((line) => theme.fg("toolOutput", line))
|
|
491
|
+
.join("\n"),
|
|
492
|
+
0,
|
|
493
|
+
0,
|
|
494
|
+
);
|
|
495
|
+
return {
|
|
496
|
+
render(width: number) {
|
|
497
|
+
const bodyRows = body.render(width);
|
|
498
|
+
const shown = bodyRows.slice(0, PLAN_PREVIEW_LINES);
|
|
499
|
+
const hidden = bodyRows.length - shown.length;
|
|
500
|
+
const header = theme.fg(
|
|
501
|
+
"muted",
|
|
502
|
+
`Plan ready · ${plan.split("\n").length} lines`,
|
|
503
|
+
);
|
|
504
|
+
const rows = [header, ...shown];
|
|
505
|
+
if (hidden > 0) {
|
|
506
|
+
// The hint gets its own row: tucking it into the header tail
|
|
507
|
+
// lets a narrow viewport clip it away along with the rest of
|
|
508
|
+
// the header, hiding the expand affordance exactly when the
|
|
509
|
+
// preview is clipped.
|
|
510
|
+
rows.push(keyHint("app.tools.expand", "to expand"));
|
|
511
|
+
rows.push(theme.fg("muted", `... (${hidden} more rows)`));
|
|
512
|
+
}
|
|
513
|
+
return rows.map((row) => truncateToWidth(row, Math.max(1, width)));
|
|
514
|
+
},
|
|
515
|
+
invalidate() {
|
|
516
|
+
body.invalidate();
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
// A plan is prose to read, not source to inspect. Without a renderer the
|
|
521
|
+
// TUI falls back to plain text and shows raw Markdown syntax.
|
|
522
|
+
return new Markdown(plan, 0, 0, getMarkdownTheme());
|
|
523
|
+
},
|
|
453
524
|
});
|
|
454
525
|
|
|
455
526
|
pi.registerCommand("plan", {
|
|
@@ -36,11 +36,13 @@ import {
|
|
|
36
36
|
POST_EDIT_COMMAND_MAX_CHARS,
|
|
37
37
|
REASONING_LEVELS,
|
|
38
38
|
SETUP_CONFIG_CHANGED_CHANNEL,
|
|
39
|
+
WEB_THEMES,
|
|
39
40
|
type FooterLayoutItem,
|
|
40
41
|
type CapabilityDiscoveryMode,
|
|
41
42
|
type FooterPreset,
|
|
42
43
|
type FooterStyle,
|
|
43
44
|
type MyPiSetupConfig,
|
|
45
|
+
type WebTheme,
|
|
44
46
|
} from "../shared/setup-config.ts";
|
|
45
47
|
|
|
46
48
|
const subagentRoleModelValueSchema = Type.Union([
|
|
@@ -107,7 +109,7 @@ export function buildInteractiveSetupPrompt(options: {
|
|
|
107
109
|
}) {
|
|
108
110
|
const configurationState = options.savedConfigExists
|
|
109
111
|
? [
|
|
110
|
-
"This package has already been configured. Explain the current settings in the user's language, then ask whether they want to keep them or change Capability discovery, Next-action suggestions, Workflow limits, UI/Footer, result detail display, Post-edit, Agent role models, or review everything.",
|
|
112
|
+
"This package has already been configured. Explain the current settings in the user's language, then ask whether they want to keep them or change Capability discovery, Next-action suggestions, Workflow limits, UI theme/Footer, result detail display, Post-edit, Agent role models, or review everything.",
|
|
111
113
|
"If the user keeps the current settings, do not call configure_my_pi_setup. If they choose a category, ask only the follow-up needed for that category.",
|
|
112
114
|
]
|
|
113
115
|
: [
|
|
@@ -130,7 +132,7 @@ export function buildInteractiveSetupPrompt(options: {
|
|
|
130
132
|
"- Capability discovery: explicit is the safe default and keeps OpenPI model tools absent until the user asks for a capability. adaptive is opt-in and keeps only the small openpi_load_tools gateway visible, allowing the model to load Subagents, Workflows, background terminals, structured search, or Session tracking when it judges them useful. Loaded groups remain session-stable, and normal permission, concurrency, and workflow limits still apply.",
|
|
131
133
|
"- Next-action suggestions: disabled, or model-generated after a fully settled main-agent run. A suggestion appears as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. Right accepts it without submitting, and any other editor input dismisses it. Enabling requires an available provider/model and reasoning level and adds one small model call per settled run.",
|
|
132
134
|
"- Workflow fan-out: concurrency controls simultaneous agents and resource pressure; max agent calls controls the total capacity of one workflow. Valid ranges are 1-64 and 1-1024.",
|
|
133
|
-
"- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with model/context on the left and git/pr/cwd on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Footer metrics use Codicon outline glyphs for model, context, and directory; a Nerd Font renders them as designed while the text stays readable without it. Changes apply immediately in the active TUI session.",
|
|
135
|
+
"- UI: the Web theme is system (default), light, or dark and is projected from this canonical configuration without browser-local overrides. The large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with model/context on the left and git/pr/cwd on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Footer metrics use Codicon outline glyphs for model, context, and directory; a Nerd Font renders them as designed while the text stays readable without it. Changes apply immediately in the active TUI session; Web theme changes apply on its next canonical snapshot.",
|
|
134
136
|
"- Operational activity for Subagents, Workflows, and background terminals is core status and always remains visible whenever the custom footer is enabled.",
|
|
135
137
|
"- Post-edit command: one optional shell command (maximum 500 characters) run in the background after a turn with successful Write/Edit operations (e.g. `npm run format`). Off by default, interactive TUI sessions only, failures surface as a notification. This is a single command, not an event-hook system.",
|
|
136
138
|
"- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full or compact; all three default to compact. Compact Subagent results show only bounded status rows and keep raw child reports behind app.tools.expand; compact Bash and Write/Edit operations use one-line semantic activity summaries. Read, grep, find, and ls use the same compact activity-row projection. Ctrl+O restores Pi's native full arguments, output, errors, diffs, and timing. Recommend compact for users who scan activity first and inspect evidence on demand.",
|
|
@@ -139,6 +141,7 @@ export function buildInteractiveSetupPrompt(options: {
|
|
|
139
141
|
"Natural-language configuration examples the user might ask for:",
|
|
140
142
|
'- "let the model discover OpenPI capabilities when useful" → capability_discovery=adaptive',
|
|
141
143
|
'- "only use OpenPI capabilities when I ask" → capability_discovery=explicit',
|
|
144
|
+
'- "use dark theme in OpenPI Web" → ui_web_theme=dark',
|
|
142
145
|
'- "switch footer to powerline" → ui_footer_preset=powerline',
|
|
143
146
|
'- "use mono powerline" → ui_footer_preset=powerline-mono',
|
|
144
147
|
'- "compact footer" → ui_footer_preset=compact',
|
|
@@ -309,7 +312,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
309
312
|
name: "configure_my_pi_setup",
|
|
310
313
|
label: "Configure OpenPI",
|
|
311
314
|
description:
|
|
312
|
-
"Apply a user-requested configuration change for this Pi setup. Configures capability discovery (explicit or opt-in adaptive), next-action suggestions, workflow fan-out, UI/Footer (presets, style, multi-line layout), result detail display, optional Post-edit, and built-in Agent-role model assignments shared by subagent_spawn and workflow agent_type. Role models must be available in the Pi registry; null clears a role back to parent-model inheritance. Footer examples: powerline preset, powerline-mono, compact, or custom ui_footer_lines with flex. Preserve current values for settings the user did not ask to change. Changes apply immediately to the capability gateway and active TUI footer.",
|
|
315
|
+
"Apply a user-requested configuration change for this Pi setup. Configures capability discovery (explicit or opt-in adaptive), next-action suggestions, workflow fan-out, the canonical OpenPI Web theme, UI/Footer (presets, style, multi-line layout), result detail display, optional Post-edit, and built-in Agent-role model assignments shared by subagent_spawn and workflow agent_type. Role models must be available in the Pi registry; null clears a role back to parent-model inheritance. Footer examples: powerline preset, powerline-mono, compact, or custom ui_footer_lines with flex. Preserve current values for settings the user did not ask to change. Changes apply immediately to the capability gateway and active TUI footer; Web observes theme changes through canonical snapshots.",
|
|
313
316
|
parameters: Type.Object({
|
|
314
317
|
capability_discovery: Type.Optional(
|
|
315
318
|
StringEnum(CAPABILITY_DISCOVERY_MODES, {
|
|
@@ -356,6 +359,12 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
356
359
|
"Whether to show the large decorative Pi header. Defaults to false; omit to preserve the current value.",
|
|
357
360
|
}),
|
|
358
361
|
),
|
|
362
|
+
ui_web_theme: Type.Optional(
|
|
363
|
+
StringEnum(WEB_THEMES, {
|
|
364
|
+
description:
|
|
365
|
+
"Canonical OpenPI Web theme: system follows the browser/OS color scheme, light and dark force that appearance. Stored in package setup rather than browser storage. Omit to preserve the current value.",
|
|
366
|
+
}),
|
|
367
|
+
),
|
|
359
368
|
ui_custom_footer: Type.Optional(
|
|
360
369
|
Type.Boolean({
|
|
361
370
|
description:
|
|
@@ -503,6 +512,9 @@ export default function openPiSetup(pi: ExtensionAPI) {
|
|
|
503
512
|
current.workflows.maxAgentCalls,
|
|
504
513
|
},
|
|
505
514
|
ui: {
|
|
515
|
+
webTheme:
|
|
516
|
+
(params.ui_web_theme as WebTheme | undefined) ??
|
|
517
|
+
current.ui.webTheme,
|
|
506
518
|
showHeader: params.ui_show_header ?? current.ui.showHeader,
|
|
507
519
|
customFooter: params.ui_custom_footer ?? current.ui.customFooter,
|
|
508
520
|
...footer,
|
|
@@ -173,9 +173,6 @@ function packageSourceValue(source: PackageSource) {
|
|
|
173
173
|
|
|
174
174
|
const CHILD_DISABLED_OPENPI_EXTENSION =
|
|
175
175
|
"-extensions/git-info/index.ts" as const;
|
|
176
|
-
const OPENPI_GIT_INFO_EXTENSION_PATH = realpathSync.native(
|
|
177
|
-
fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
|
|
178
|
-
);
|
|
179
176
|
|
|
180
177
|
function canonicalExistingPath(value: string) {
|
|
181
178
|
try {
|
|
@@ -185,15 +182,38 @@ function canonicalExistingPath(value: string) {
|
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
184
|
|
|
185
|
+
/**
|
|
186
|
+
* ENOENT means git-info is truly absent (trimmed fork, partial install):
|
|
187
|
+
* nothing to exclude. Any other failure (permissions, symlink loops) means we
|
|
188
|
+
* cannot verify the Git-polling extension is present, so fail closed instead
|
|
189
|
+
* of running a child with an unverifiable extension.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveGitInfoPathOrThrow(
|
|
192
|
+
resolve: (value: string) => string = realpathSync.native,
|
|
193
|
+
): string | undefined {
|
|
194
|
+
try {
|
|
195
|
+
return resolve(
|
|
196
|
+
fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
|
|
197
|
+
);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
188
204
|
function excludeOpenPiGitInfoExtension(
|
|
189
205
|
resources: LoadExtensionsResult,
|
|
206
|
+
resolve: (value: string) => string = realpathSync.native,
|
|
190
207
|
): LoadExtensionsResult {
|
|
208
|
+
const gitInfoPath = resolveGitInfoPathOrThrow(resolve);
|
|
209
|
+
// undefined = absent (ENOENT): nothing to exclude. Non-ENOENT failures
|
|
210
|
+
// throw from resolveGitInfoPathOrThrow and fail child creation upstream.
|
|
211
|
+
if (gitInfoPath === undefined) return resources;
|
|
191
212
|
return {
|
|
192
213
|
...resources,
|
|
193
214
|
extensions: resources.extensions.filter(
|
|
194
215
|
(extension) =>
|
|
195
|
-
canonicalExistingPath(extension.resolvedPath) !==
|
|
196
|
-
OPENPI_GIT_INFO_EXTENSION_PATH,
|
|
216
|
+
canonicalExistingPath(extension.resolvedPath) !== gitInfoPath,
|
|
197
217
|
),
|
|
198
218
|
};
|
|
199
219
|
}
|