@tt-a1i/openpi 0.2.0 → 0.3.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 +28 -6
- package/SETUP.md +1 -1
- package/extensions/plan-mode/bash-policy.ts +219 -42
- package/extensions/plan-mode/index.ts +44 -19
- package/extensions/setup/index.ts +3 -3
- package/extensions/shared/editor-layers.ts +150 -0
- package/extensions/shared/setup-config.ts +4 -15
- package/extensions/subagents/index.ts +164 -96
- package/extensions/subagents/src/prompt.ts +6 -6
- package/extensions/subagents/src/ui/takeover.ts +231 -133
- package/extensions/subagents/src/ui/transcript.ts +252 -37
- package/extensions/subagents/src/ui/wait-result.ts +6 -19
- package/extensions/suggestions/index.ts +27 -18
- package/extensions/tasks/index.ts +24 -6
- package/extensions/ui-customization/footer.ts +59 -10
- package/extensions/workflows/index.ts +28 -20
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +1 -1
|
@@ -18,6 +18,29 @@ import type { SubagentSnapshot, TranscriptItem } from "../domain.ts";
|
|
|
18
18
|
|
|
19
19
|
const MAX_CACHED_WIDTHS_PER_ITEM = 2;
|
|
20
20
|
|
|
21
|
+
export const SPINNER_FRAMES = [
|
|
22
|
+
"⠋",
|
|
23
|
+
"⠙",
|
|
24
|
+
"⠹",
|
|
25
|
+
"⠸",
|
|
26
|
+
"⠼",
|
|
27
|
+
"⠴",
|
|
28
|
+
"⠦",
|
|
29
|
+
"⠧",
|
|
30
|
+
"⠇",
|
|
31
|
+
"⠏",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** Frame cadence, shared with the dashboard and takeover headers. */
|
|
35
|
+
export const SPINNER_INTERVAL_MS = 120;
|
|
36
|
+
|
|
37
|
+
export function spinnerFrame(now: number) {
|
|
38
|
+
const frame = Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
|
|
39
|
+
return SPINNER_FRAMES[
|
|
40
|
+
(frame + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
/**
|
|
22
45
|
* Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
|
|
23
46
|
* tabs (and stray escapes) make lines wider than the width we declare to the
|
|
@@ -146,10 +169,82 @@ function renderThinking(theme: Theme, text: string, width: number) {
|
|
|
146
169
|
return out;
|
|
147
170
|
}
|
|
148
171
|
|
|
172
|
+
function renderToolBody(theme: Theme, name: string, argsPreview?: string) {
|
|
173
|
+
const toolName = sanitizeText(name);
|
|
174
|
+
const preview = summarizeToolArgs(toolName, argsPreview);
|
|
175
|
+
// The `$` form only earns its prompt when there is a command to show; a bare
|
|
176
|
+
// `$ ` would read as an empty shell line.
|
|
177
|
+
if (toolName === "bash" && preview) return theme.fg("dim", `$ ${preview}`);
|
|
178
|
+
return (
|
|
179
|
+
theme.fg("dim", "→ ") +
|
|
180
|
+
theme.fg("toolTitle", toolName) +
|
|
181
|
+
(preview ? theme.fg("dim", ` ${preview}`) : "")
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function firstOutputPreview(outputPreview?: string) {
|
|
186
|
+
return (
|
|
187
|
+
sanitizeText(outputPreview ?? "")
|
|
188
|
+
.split("\n")
|
|
189
|
+
.find((line) => line.trim()) ?? ""
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* One execution owns exactly one glyph column, on its command line. Output
|
|
195
|
+
* lines are plain indented text so a block keeps identical columns from the
|
|
196
|
+
* moment the command starts to the moment it settles.
|
|
197
|
+
*/
|
|
198
|
+
export type ToolPhase = "live" | "ok" | "error" | "pending";
|
|
199
|
+
|
|
200
|
+
function phaseGlyph(theme: Theme, phase: ToolPhase, now: number) {
|
|
201
|
+
switch (phase) {
|
|
202
|
+
case "live":
|
|
203
|
+
return theme.fg("warning", spinnerFrame(now));
|
|
204
|
+
case "ok":
|
|
205
|
+
return theme.fg("success", "✓");
|
|
206
|
+
case "error":
|
|
207
|
+
return theme.fg("error", "✗");
|
|
208
|
+
case "pending":
|
|
209
|
+
return theme.fg("dim", "·");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Command line: `<glyph> $ cmd` for bash, `<glyph> → name args` otherwise. */
|
|
214
|
+
function renderToolLine(
|
|
215
|
+
theme: Theme,
|
|
216
|
+
phase: ToolPhase,
|
|
217
|
+
name: string,
|
|
218
|
+
argsPreview: string | undefined,
|
|
219
|
+
width: number,
|
|
220
|
+
now: number,
|
|
221
|
+
) {
|
|
222
|
+
return truncateToWidth(
|
|
223
|
+
`${phaseGlyph(theme, phase, now)} ${renderToolBody(theme, name, argsPreview)}`,
|
|
224
|
+
width,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Output line: indented under the command, no second glyph. */
|
|
229
|
+
function renderOutputLine(
|
|
230
|
+
theme: Theme,
|
|
231
|
+
isError: boolean,
|
|
232
|
+
outputPreview: string,
|
|
233
|
+
width: number,
|
|
234
|
+
) {
|
|
235
|
+
const preview = outputPreview || "(no output)";
|
|
236
|
+
const content = isError
|
|
237
|
+
? theme.fg(outputPreview ? "error" : "dim", preview)
|
|
238
|
+
: theme.fg("dim", preview);
|
|
239
|
+
return truncateToWidth(` ${content}`, width);
|
|
240
|
+
}
|
|
241
|
+
|
|
149
242
|
function renderAssistantItem(
|
|
150
243
|
theme: Theme,
|
|
151
244
|
item: Extract<TranscriptItem, { kind: "assistant" }>,
|
|
152
245
|
width: number,
|
|
246
|
+
phases: ReadonlyMap<string, ToolPhase>,
|
|
247
|
+
now: number,
|
|
153
248
|
) {
|
|
154
249
|
const out: string[] = [];
|
|
155
250
|
for (const part of item.parts) {
|
|
@@ -164,12 +259,14 @@ function renderAssistantItem(
|
|
|
164
259
|
),
|
|
165
260
|
);
|
|
166
261
|
} else if (part.type === "toolCall") {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
out.push(
|
|
262
|
+
const phase = phases.get(part.toolId) ?? "pending";
|
|
263
|
+
// A live tool is rendered by the live block, which owns the spinner and
|
|
264
|
+
// the streaming output; rendering the call here too would show the same
|
|
265
|
+
// command twice and make the block reflow when the tool settles.
|
|
266
|
+
if (phase === "live") continue;
|
|
267
|
+
out.push(
|
|
268
|
+
renderToolLine(theme, phase, part.name, part.argsPreview, width, now),
|
|
269
|
+
);
|
|
173
270
|
}
|
|
174
271
|
}
|
|
175
272
|
return out;
|
|
@@ -179,27 +276,117 @@ function renderToolResultItem(
|
|
|
179
276
|
theme: Theme,
|
|
180
277
|
item: Extract<TranscriptItem, { kind: "toolResult" }>,
|
|
181
278
|
width: number,
|
|
279
|
+
paired: boolean,
|
|
280
|
+
now: number,
|
|
182
281
|
) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
282
|
+
const preview = firstOutputPreview(item.outputPreview);
|
|
283
|
+
// An orphan result (its call is not the previous item) still needs a glyph:
|
|
284
|
+
// there is no command line above it to carry one.
|
|
285
|
+
if (!paired) {
|
|
286
|
+
return [
|
|
287
|
+
renderToolLine(
|
|
288
|
+
theme,
|
|
289
|
+
item.isError ? "error" : "ok",
|
|
290
|
+
item.name,
|
|
291
|
+
undefined,
|
|
292
|
+
width,
|
|
293
|
+
now,
|
|
294
|
+
),
|
|
295
|
+
...(preview
|
|
296
|
+
? [renderOutputLine(theme, item.isError, preview, width)]
|
|
297
|
+
: []),
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
return [renderOutputLine(theme, item.isError, preview, width)];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isPairedToolResult(
|
|
304
|
+
previous: TranscriptItem | undefined,
|
|
305
|
+
current: TranscriptItem,
|
|
306
|
+
) {
|
|
307
|
+
if (
|
|
308
|
+
!previous ||
|
|
309
|
+
previous.kind !== "assistant" ||
|
|
310
|
+
current.kind !== "toolResult"
|
|
311
|
+
) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const lastPart = previous.parts[previous.parts.length - 1];
|
|
315
|
+
return lastPart?.type === "toolCall" && lastPart.toolId === current.toolId;
|
|
193
316
|
}
|
|
194
317
|
|
|
195
318
|
function renderTranscriptItem(
|
|
196
319
|
theme: Theme,
|
|
197
320
|
item: TranscriptItem,
|
|
198
321
|
width: number,
|
|
322
|
+
context: ItemContext,
|
|
323
|
+
now: number,
|
|
199
324
|
) {
|
|
200
325
|
if (item.kind === "user") return renderUserText(theme, item.text, width);
|
|
201
|
-
if (item.kind === "assistant")
|
|
202
|
-
|
|
326
|
+
if (item.kind === "assistant") {
|
|
327
|
+
return renderAssistantItem(theme, item, width, context.phases, now);
|
|
328
|
+
}
|
|
329
|
+
return renderToolResultItem(theme, item, width, context.paired, now);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
interface ItemContext {
|
|
333
|
+
readonly phases: ReadonlyMap<string, ToolPhase>;
|
|
334
|
+
readonly paired: boolean;
|
|
335
|
+
/** Cache discriminator: identity plus width is not enough on its own. */
|
|
336
|
+
readonly token: string;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* An item's rendering depends on its neighbours (does a call have its result
|
|
341
|
+
* yet?) and on live state (is the call still running?), so the cache key has to
|
|
342
|
+
* carry that context or a stale glyph would outlive the phase it described.
|
|
343
|
+
*/
|
|
344
|
+
function itemContext(
|
|
345
|
+
transcript: ReadonlyArray<TranscriptItem>,
|
|
346
|
+
index: number,
|
|
347
|
+
liveIds: ReadonlySet<string>,
|
|
348
|
+
): ItemContext {
|
|
349
|
+
const item = transcript[index]!;
|
|
350
|
+
if (item.kind === "user")
|
|
351
|
+
return { phases: new Map(), paired: false, token: "" };
|
|
352
|
+
if (item.kind === "toolResult") {
|
|
353
|
+
const paired = isPairedToolResult(transcript[index - 1], item);
|
|
354
|
+
return { phases: new Map(), paired, token: paired ? "p" : "o" };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const phases = new Map<string, ToolPhase>();
|
|
358
|
+
for (const part of item.parts) {
|
|
359
|
+
if (part.type !== "toolCall") continue;
|
|
360
|
+
if (liveIds.has(part.toolId)) {
|
|
361
|
+
phases.set(part.toolId, "live");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const result = findResult(transcript, index, part.toolId);
|
|
365
|
+
phases.set(
|
|
366
|
+
part.toolId,
|
|
367
|
+
result ? (result.isError ? "error" : "ok") : "pending",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
phases,
|
|
372
|
+
paired: false,
|
|
373
|
+
token: [...phases].map(([id, phase]) => `${id}:${phase}`).join(","),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The result for a call, if it has already landed later in the transcript. */
|
|
378
|
+
function findResult(
|
|
379
|
+
transcript: ReadonlyArray<TranscriptItem>,
|
|
380
|
+
callIndex: number,
|
|
381
|
+
toolId: string,
|
|
382
|
+
) {
|
|
383
|
+
for (let index = callIndex + 1; index < transcript.length; index++) {
|
|
384
|
+
const candidate = transcript[index];
|
|
385
|
+
if (candidate?.kind === "toolResult" && candidate.toolId === toolId) {
|
|
386
|
+
return candidate;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
203
390
|
}
|
|
204
391
|
|
|
205
392
|
/**
|
|
@@ -208,24 +395,43 @@ function renderTranscriptItem(
|
|
|
208
395
|
* from their component's invalidate() when Pi changes theme.
|
|
209
396
|
*/
|
|
210
397
|
export class TranscriptRenderer {
|
|
211
|
-
private itemCache = new WeakMap<TranscriptItem, Map<
|
|
398
|
+
private itemCache = new WeakMap<TranscriptItem, Map<string, string[]>>();
|
|
212
399
|
|
|
213
|
-
render(
|
|
400
|
+
render(
|
|
401
|
+
snap: SubagentSnapshot,
|
|
402
|
+
width: number,
|
|
403
|
+
theme: Theme,
|
|
404
|
+
options?: { readonly now?: number },
|
|
405
|
+
) {
|
|
214
406
|
const out: string[] = [];
|
|
407
|
+
const now = options?.now ?? Date.now();
|
|
408
|
+
const liveIds = new Set(snap.liveTools.map((tool) => tool.toolId));
|
|
215
409
|
|
|
216
|
-
for (
|
|
217
|
-
const
|
|
218
|
-
const
|
|
410
|
+
for (let index = 0; index < snap.transcript.length; index++) {
|
|
411
|
+
const item = snap.transcript[index];
|
|
412
|
+
const context = itemContext(snap.transcript, index, liveIds);
|
|
413
|
+
const key = `${width}|${context.token}`;
|
|
414
|
+
const cached = this.itemCache.get(item)?.get(key);
|
|
415
|
+
const lines =
|
|
416
|
+
cached ?? renderTranscriptItem(theme, item, width, context, now);
|
|
219
417
|
if (!cached) {
|
|
220
|
-
const widths = this.itemCache.get(item) ?? new Map<
|
|
418
|
+
const widths = this.itemCache.get(item) ?? new Map<string, string[]>();
|
|
221
419
|
if (widths.size >= MAX_CACHED_WIDTHS_PER_ITEM) {
|
|
222
420
|
const oldestWidth = widths.keys().next().value;
|
|
223
421
|
if (oldestWidth !== undefined) widths.delete(oldestWidth);
|
|
224
422
|
}
|
|
225
|
-
widths.set(
|
|
423
|
+
widths.set(key, lines);
|
|
226
424
|
this.itemCache.set(item, widths);
|
|
227
425
|
}
|
|
228
|
-
if (lines.length > 0)
|
|
426
|
+
if (lines.length > 0) {
|
|
427
|
+
if (
|
|
428
|
+
out.length > 0 &&
|
|
429
|
+
!isPairedToolResult(snap.transcript[index - 1], item)
|
|
430
|
+
) {
|
|
431
|
+
out.push("");
|
|
432
|
+
}
|
|
433
|
+
out.push(...lines);
|
|
434
|
+
}
|
|
229
435
|
}
|
|
230
436
|
while (out.length > 0 && out[out.length - 1] === "") out.pop();
|
|
231
437
|
|
|
@@ -239,19 +445,22 @@ export class TranscriptRenderer {
|
|
|
239
445
|
if (out.length === before + 1) out.pop();
|
|
240
446
|
}
|
|
241
447
|
|
|
242
|
-
// Live tool executions
|
|
448
|
+
// Live tool executions. The manager drops a live entry when its ToolEnd
|
|
449
|
+
// lands, and the transcript's call line then takes over with the settled
|
|
450
|
+
// glyph in the same column, so the block never reflows.
|
|
243
451
|
for (const tool of snap.liveTools) {
|
|
244
452
|
if (out.length > 0) out.push("");
|
|
245
|
-
const
|
|
453
|
+
const phase: ToolPhase = tool.done
|
|
246
454
|
? tool.isError
|
|
247
|
-
?
|
|
248
|
-
:
|
|
249
|
-
:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
455
|
+
? "error"
|
|
456
|
+
: "ok"
|
|
457
|
+
: "live";
|
|
458
|
+
out.push(
|
|
459
|
+
renderToolLine(theme, phase, tool.name, tool.argsPreview, width, now),
|
|
460
|
+
);
|
|
461
|
+
const preview = firstOutputPreview(tool.outputPreview);
|
|
462
|
+
if (preview)
|
|
463
|
+
out.push(renderOutputLine(theme, !!tool.isError, preview, width));
|
|
255
464
|
}
|
|
256
465
|
|
|
257
466
|
// Queued steering/follow-up messages: show them immediately so Enter
|
|
@@ -288,6 +497,12 @@ export function buildTranscriptLines(
|
|
|
288
497
|
width: number,
|
|
289
498
|
theme: Theme,
|
|
290
499
|
renderer?: TranscriptRenderer,
|
|
500
|
+
options?: { readonly now?: number },
|
|
291
501
|
) {
|
|
292
|
-
return (renderer ?? new TranscriptRenderer()).render(
|
|
502
|
+
return (renderer ?? new TranscriptRenderer()).render(
|
|
503
|
+
snap,
|
|
504
|
+
width,
|
|
505
|
+
theme,
|
|
506
|
+
options,
|
|
507
|
+
);
|
|
293
508
|
}
|
|
@@ -6,17 +6,16 @@ import {
|
|
|
6
6
|
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import { sanitizeText } from "./transcript.ts";
|
|
8
8
|
|
|
9
|
-
const COLLAPSED_MAX_LINES = 12;
|
|
10
9
|
const MAX_STATUS_ROWS = 4;
|
|
11
10
|
|
|
12
11
|
export interface WaitResultItem {
|
|
13
|
-
id: string;
|
|
14
|
-
title?: string;
|
|
15
|
-
status?: string;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly title?: string;
|
|
14
|
+
readonly status?: string;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export interface WaitResultDetails {
|
|
19
|
-
results?: WaitResultItem[];
|
|
18
|
+
readonly results?: readonly WaitResultItem[];
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function buildWaitResultPreview(
|
|
@@ -46,23 +45,11 @@ export function buildWaitResultPreview(
|
|
|
46
45
|
lines.push(theme.fg("dim", ` … ${results.length - MAX_STATUS_ROWS} more`));
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
.split("\n")
|
|
51
|
-
.map((line) => line.trimEnd())
|
|
52
|
-
.filter((line) => line.trim() && line.trim() !== "---");
|
|
53
|
-
const leadingHeader = cleanLines[0]?.startsWith("## ") ? 1 : 0;
|
|
54
|
-
const body = cleanLines.slice(leadingHeader);
|
|
55
|
-
const available = Math.max(1, COLLAPSED_MAX_LINES - lines.length - 1);
|
|
56
|
-
for (const line of body.slice(0, available)) {
|
|
57
|
-
lines.push(theme.fg("toolOutput", line));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const hidden = Math.max(0, body.length - available);
|
|
61
|
-
if (hidden > 0) {
|
|
48
|
+
if (content.trim()) {
|
|
62
49
|
lines.push(
|
|
63
50
|
theme.fg(
|
|
64
51
|
"dim",
|
|
65
|
-
|
|
52
|
+
`Results passed to main agent · ${keyHint("app.tools.expand", "to expand")}`,
|
|
66
53
|
),
|
|
67
54
|
);
|
|
68
55
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type ExtensionContext,
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
5
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
registerEditorLayer,
|
|
7
|
+
removeEditorLayer,
|
|
8
|
+
} from "../shared/editor-layers.ts";
|
|
6
9
|
import {
|
|
7
10
|
loadSetupConfig,
|
|
8
11
|
SETUP_CONFIG_CHANGED_CHANNEL,
|
|
@@ -51,6 +54,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
51
54
|
let sessionActive = false;
|
|
52
55
|
let statusContext: ExtensionContext | undefined;
|
|
53
56
|
let requestEditorRender: (() => void) | undefined;
|
|
57
|
+
let editorLayerRegistered = false;
|
|
54
58
|
|
|
55
59
|
const updateStatus = () => {
|
|
56
60
|
statusContext?.ui.setStatus(
|
|
@@ -72,21 +76,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
72
76
|
|
|
73
77
|
const installSuggestionEditor = (ctx: ExtensionContext) => {
|
|
74
78
|
if (ctx.mode !== "tui") return;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
registerEditorLayer(pi, ctx, {
|
|
80
|
+
id: "suggestions",
|
|
81
|
+
order: 200,
|
|
82
|
+
wrap: (base, tui, _theme, keybindings) => {
|
|
83
|
+
requestEditorRender = () => tui.requestRender();
|
|
84
|
+
return new NextActionSuggestionEditor(
|
|
85
|
+
base,
|
|
86
|
+
keybindings,
|
|
87
|
+
suggestionState,
|
|
88
|
+
cancelPrediction,
|
|
89
|
+
requestEditorRender,
|
|
90
|
+
(text) => ctx.ui.theme.fg("dim", text),
|
|
91
|
+
);
|
|
92
|
+
},
|
|
89
93
|
});
|
|
94
|
+
editorLayerRegistered = true;
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -160,6 +165,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
160
165
|
pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, cancelPrediction);
|
|
161
166
|
|
|
162
167
|
pi.on("session_shutdown", async () => {
|
|
168
|
+
if (editorLayerRegistered) {
|
|
169
|
+
removeEditorLayer(pi, "suggestions");
|
|
170
|
+
editorLayerRegistered = false;
|
|
171
|
+
}
|
|
163
172
|
sessionActive = false;
|
|
164
173
|
runBoundary.reset();
|
|
165
174
|
const task = activePrediction?.task;
|
|
@@ -99,6 +99,8 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
99
99
|
let notifiedProblem: string | undefined;
|
|
100
100
|
let taskWidgetVisible = true;
|
|
101
101
|
let taskWidgetExpanded = false;
|
|
102
|
+
let taskWidgetMounted = false;
|
|
103
|
+
let requestTaskWidgetRender: (() => void) | undefined;
|
|
102
104
|
let ui: ExtensionContext["ui"] | undefined;
|
|
103
105
|
let uiMode: ExtensionContext["mode"] | undefined;
|
|
104
106
|
const hideLifecycleTools = () =>
|
|
@@ -124,22 +126,36 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
124
126
|
|
|
125
127
|
const updateTaskWidget = (ctx?: ExtensionContext) => {
|
|
126
128
|
if (ctx?.hasUI) {
|
|
129
|
+
if (ui !== ctx.ui) {
|
|
130
|
+
taskWidgetMounted = false;
|
|
131
|
+
requestTaskWidgetRender = undefined;
|
|
132
|
+
}
|
|
127
133
|
ui = ctx.ui;
|
|
128
134
|
uiMode = ctx.mode;
|
|
129
135
|
}
|
|
130
136
|
if (!ui || uiMode !== "tui") return false;
|
|
131
|
-
const current = snapshot();
|
|
132
137
|
const shown =
|
|
133
138
|
taskWidgetVisible && !problemMessage() && hasActionableTasks();
|
|
134
139
|
if (!shown) {
|
|
140
|
+
if (!taskWidgetMounted) return false;
|
|
135
141
|
ui.setWidget(TASK_WIDGET_KEY, undefined);
|
|
142
|
+
taskWidgetMounted = false;
|
|
143
|
+
requestTaskWidgetRender = undefined;
|
|
136
144
|
return false;
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
if (taskWidgetMounted) {
|
|
147
|
+
requestTaskWidgetRender?.();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
ui.setWidget(TASK_WIDGET_KEY, (tui, theme) => {
|
|
151
|
+
requestTaskWidgetRender = () => tui.requestRender();
|
|
152
|
+
return {
|
|
153
|
+
render: (width) =>
|
|
154
|
+
renderTaskWidget(snapshot(), theme, width, taskWidgetExpanded),
|
|
155
|
+
invalidate() {},
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
taskWidgetMounted = true;
|
|
143
159
|
return true;
|
|
144
160
|
};
|
|
145
161
|
|
|
@@ -519,6 +535,8 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
519
535
|
} catch {
|
|
520
536
|
// The interactive UI may already be disposed.
|
|
521
537
|
}
|
|
538
|
+
taskWidgetMounted = false;
|
|
539
|
+
requestTaskWidgetRender = undefined;
|
|
522
540
|
ui = undefined;
|
|
523
541
|
uiMode = undefined;
|
|
524
542
|
});
|
|
@@ -450,27 +450,76 @@ export function renderFooter(options: FooterRenderOptions) {
|
|
|
450
450
|
formatPullRequest,
|
|
451
451
|
);
|
|
452
452
|
const lines: string[] = [];
|
|
453
|
+
const statusLines = options.statuses
|
|
454
|
+
? Array.from(options.statuses).flatMap((status) => status.split("\n"))
|
|
455
|
+
: [];
|
|
456
|
+
const singleStatus = statusLines.length === 1 ? statusLines[0] : undefined;
|
|
457
|
+
const separator = options.theme.fg("dim", " · ");
|
|
458
|
+
const styledStatus =
|
|
459
|
+
singleStatus === undefined
|
|
460
|
+
? undefined
|
|
461
|
+
: options.theme.fg("dim", singleStatus);
|
|
462
|
+
const statusWidth =
|
|
463
|
+
styledStatus === undefined
|
|
464
|
+
? 0
|
|
465
|
+
: visibleWidth(separator) + visibleWidth(styledStatus);
|
|
466
|
+
let inlinedStatus = false;
|
|
453
467
|
|
|
454
468
|
for (const layout of options.lines) {
|
|
455
|
-
const
|
|
469
|
+
const resolved = resolveLineSegments(
|
|
456
470
|
layout,
|
|
457
471
|
catalog,
|
|
458
472
|
options.style,
|
|
473
|
+
options.modelInfo.contextPercent,
|
|
474
|
+
);
|
|
475
|
+
const fitted = fitSegmentsToWidth(
|
|
476
|
+
resolved.left,
|
|
477
|
+
resolved.right,
|
|
459
478
|
options.width,
|
|
479
|
+
options.style,
|
|
480
|
+
options.theme,
|
|
481
|
+
);
|
|
482
|
+
const canInlineStatus =
|
|
483
|
+
lines.length === 0 &&
|
|
484
|
+
styledStatus !== undefined &&
|
|
485
|
+
naturalLineWidth(
|
|
486
|
+
fitted.left,
|
|
487
|
+
fitted.right,
|
|
488
|
+
options.style,
|
|
489
|
+
options.theme,
|
|
490
|
+
) +
|
|
491
|
+
statusWidth <=
|
|
492
|
+
options.width;
|
|
493
|
+
const line = renderFooterLine(
|
|
494
|
+
layout,
|
|
495
|
+
catalog,
|
|
496
|
+
options.style,
|
|
497
|
+
canInlineStatus ? options.width - statusWidth : options.width,
|
|
460
498
|
options.theme,
|
|
461
499
|
options.modelInfo.contextPercent,
|
|
462
500
|
);
|
|
463
|
-
if (line)
|
|
501
|
+
if (!line) continue;
|
|
502
|
+
lines.push(
|
|
503
|
+
canInlineStatus
|
|
504
|
+
? truncateToWidth(
|
|
505
|
+
`${line}${separator}${styledStatus}`,
|
|
506
|
+
options.width,
|
|
507
|
+
options.theme.fg("dim", "..."),
|
|
508
|
+
)
|
|
509
|
+
: line,
|
|
510
|
+
);
|
|
511
|
+
inlinedStatus ||= canInlineStatus;
|
|
464
512
|
}
|
|
465
513
|
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
514
|
+
if (inlinedStatus) return lines;
|
|
515
|
+
for (const statusLine of statusLines) {
|
|
516
|
+
lines.push(
|
|
517
|
+
truncateToWidth(
|
|
518
|
+
statusLine,
|
|
519
|
+
options.width,
|
|
520
|
+
options.theme.fg("dim", "..."),
|
|
521
|
+
),
|
|
522
|
+
);
|
|
474
523
|
}
|
|
475
524
|
|
|
476
525
|
return lines;
|