@tt-a1i/openpi 0.3.1 → 0.5.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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Only the two paint calls this chrome makes, following the footer's precedent:
|
|
6
|
+
* views that carry a narrowed theme object can use it without a cast.
|
|
7
|
+
*/
|
|
8
|
+
type Theme = Pick<ExtensionContext["ui"]["theme"], "fg" | "bold">;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Some labels arrive already styled (a task census colours each state). Painting
|
|
12
|
+
* a colour over them would apply only up to their first inner reset, leaving the
|
|
13
|
+
* rest a different shade than the caller asked for, so pre-styled text is passed
|
|
14
|
+
* through untouched.
|
|
15
|
+
*/
|
|
16
|
+
const isStyled = (text: string) => text.includes("\u001b");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The chrome every full-screen OpenPI view shares: a title line, one bordered
|
|
20
|
+
* panel, a hint line, and one way to say "there is more above/below".
|
|
21
|
+
*
|
|
22
|
+
* Three views had grown their own copy of this (`/subagents`, `/ps`,
|
|
23
|
+
* `/workflows`), and they had drifted apart in the details you notice without
|
|
24
|
+
* being able to name: one framed the body in `border`, another in
|
|
25
|
+
* `borderMuted`; one wrote `... 3 more` and another `… 3 more`; hints were a
|
|
26
|
+
* single dim run in which the keys you are supposed to press read exactly as
|
|
27
|
+
* faint as the prose describing them. Fixing that in one place is also the only
|
|
28
|
+
* way it stays fixed.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Title left, quiet census right, with the same one-column inset as the panel. */
|
|
32
|
+
export function screenTitleLine(
|
|
33
|
+
theme: Theme,
|
|
34
|
+
title: string,
|
|
35
|
+
meta: string,
|
|
36
|
+
width: number,
|
|
37
|
+
) {
|
|
38
|
+
const left = ` ${theme.bold(theme.fg("accent", title))}`;
|
|
39
|
+
const right = meta ? `${isStyled(meta) ? meta : theme.fg("dim", meta)} ` : "";
|
|
40
|
+
const rightWidth = visibleWidth(right);
|
|
41
|
+
const fittedLeft = truncateToWidth(
|
|
42
|
+
left,
|
|
43
|
+
Math.max(0, width - rightWidth - 1),
|
|
44
|
+
"…",
|
|
45
|
+
);
|
|
46
|
+
const pad = Math.max(1, width - visibleWidth(fittedLeft) - rightWidth);
|
|
47
|
+
return truncateToWidth(fittedLeft + " ".repeat(pad) + right, width, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Bordered panel with an optional label set into the top edge, padded to an
|
|
52
|
+
* exact height so a view's overlay never changes size as content streams in.
|
|
53
|
+
*/
|
|
54
|
+
export function panelFrame(
|
|
55
|
+
theme: Theme,
|
|
56
|
+
options: {
|
|
57
|
+
label?: string;
|
|
58
|
+
rows: readonly string[];
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
},
|
|
62
|
+
) {
|
|
63
|
+
const { label = "", rows, width, height } = options;
|
|
64
|
+
const inner = Math.max(0, width - 2);
|
|
65
|
+
const border = (text: string) => theme.fg("borderMuted", text);
|
|
66
|
+
// The label rides the border rather than sitting above it, so it reads as
|
|
67
|
+
// this panel's name instead of competing with the screen title.
|
|
68
|
+
const clippedLabel = label
|
|
69
|
+
? truncateToWidth(` ${label} `, Math.max(0, inner - 2))
|
|
70
|
+
: "";
|
|
71
|
+
const labelText = clippedLabel
|
|
72
|
+
? isStyled(clippedLabel)
|
|
73
|
+
? clippedLabel
|
|
74
|
+
: theme.fg("muted", clippedLabel)
|
|
75
|
+
: "";
|
|
76
|
+
const dashes = Math.max(0, inner - visibleWidth(labelText) - 1);
|
|
77
|
+
const lines = [border("╭─") + labelText + border("─".repeat(dashes) + "╮")];
|
|
78
|
+
const bodyHeight = Math.max(0, height - 2);
|
|
79
|
+
for (let index = 0; index < bodyHeight; index += 1) {
|
|
80
|
+
const clipped = truncateToWidth(rows[index] ?? "", inner, "…");
|
|
81
|
+
const pad = Math.max(0, inner - visibleWidth(clipped));
|
|
82
|
+
lines.push(border("│") + clipped + " ".repeat(pad) + border("│"));
|
|
83
|
+
}
|
|
84
|
+
lines.push(border("╰" + "─".repeat(inner) + "╯"));
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One `keys label` pair of a hint line. */
|
|
89
|
+
export type ScreenHint = readonly [keys: string, label: string];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Keys read one step brighter than what they do, so the line scans as a
|
|
93
|
+
* keyboard legend instead of a sentence. A notice takes the whole line when
|
|
94
|
+
* there is one: it is the answer to what you just pressed, and the legend can
|
|
95
|
+
* wait a beat.
|
|
96
|
+
*/
|
|
97
|
+
export function hintLine(
|
|
98
|
+
theme: Theme,
|
|
99
|
+
hints: readonly (ScreenHint | undefined)[],
|
|
100
|
+
width: number,
|
|
101
|
+
notice?: string,
|
|
102
|
+
) {
|
|
103
|
+
if (notice) {
|
|
104
|
+
return truncateToWidth(theme.fg("accent", ` ${notice}`), width, "…");
|
|
105
|
+
}
|
|
106
|
+
const parts = hints
|
|
107
|
+
.filter((hint): hint is ScreenHint => Boolean(hint))
|
|
108
|
+
.map(([keys, label]) => {
|
|
109
|
+
// An empty key slot is a plain status segment (a scroll position, say);
|
|
110
|
+
// an empty label is a bare key. Both stay dimmer than a real key.
|
|
111
|
+
if (!keys) return theme.fg("dim", label);
|
|
112
|
+
if (!label) return theme.fg("muted", keys);
|
|
113
|
+
return `${theme.fg("muted", keys)} ${theme.fg("dim", label)}`;
|
|
114
|
+
});
|
|
115
|
+
return truncateToWidth(
|
|
116
|
+
` ${parts.join(theme.fg("dim", " · "))}`,
|
|
117
|
+
width,
|
|
118
|
+
theme.fg("dim", "…"),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Single vocabulary for a clipped list: `… 3 more agents`. */
|
|
123
|
+
export function overflowNote(
|
|
124
|
+
theme: Theme,
|
|
125
|
+
count: number,
|
|
126
|
+
width: number,
|
|
127
|
+
noun = "",
|
|
128
|
+
) {
|
|
129
|
+
return truncateToWidth(
|
|
130
|
+
theme.fg("dim", ` … ${count} more${noun ? ` ${noun}` : ""}`),
|
|
131
|
+
width,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -69,7 +69,28 @@ export type CapabilityDiscoveryMode =
|
|
|
69
69
|
|
|
70
70
|
/** Canonical default layout: one-line plain footer with flex alignment. */
|
|
71
71
|
export const DEFAULT_FOOTER_LINES: FooterLines = [
|
|
72
|
-
["
|
|
72
|
+
["model", "context", "flex", "git", "pr", "cwd"],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/** Stable skeleton for the legacy flat-selection adapter. */
|
|
76
|
+
const FLAT_FOOTER_ITEM_LINES: FooterLines = [
|
|
77
|
+
[
|
|
78
|
+
"model",
|
|
79
|
+
"thinking",
|
|
80
|
+
"context",
|
|
81
|
+
"cache",
|
|
82
|
+
"cost",
|
|
83
|
+
"throughput",
|
|
84
|
+
"flex",
|
|
85
|
+
"git",
|
|
86
|
+
"pr",
|
|
87
|
+
"cwd",
|
|
88
|
+
],
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/** The persisted canonical default before model/context moved to the left. */
|
|
92
|
+
const LEGACY_DEFAULT_FOOTER_LINES: FooterLines = [
|
|
93
|
+
["cwd", "git", "pr", "flex", "model", "context"],
|
|
73
94
|
];
|
|
74
95
|
|
|
75
96
|
export const DEFAULT_FOOTER_STYLE: FooterStyle = "plain";
|
|
@@ -88,7 +109,7 @@ export const FOOTER_PRESET_DEFINITIONS: Record<
|
|
|
88
109
|
> = {
|
|
89
110
|
compact: {
|
|
90
111
|
style: "plain",
|
|
91
|
-
lines:
|
|
112
|
+
lines: DEFAULT_FOOTER_LINES,
|
|
92
113
|
},
|
|
93
114
|
powerline: {
|
|
94
115
|
style: "powerline",
|
|
@@ -131,9 +152,7 @@ export interface MyPiSetupConfig {
|
|
|
131
152
|
readonly showHeader: boolean;
|
|
132
153
|
readonly customFooter: boolean;
|
|
133
154
|
readonly footerStyle: FooterStyle;
|
|
134
|
-
/** Canonical footer layout. `footerItems` is always derived from this. */
|
|
135
155
|
readonly footerLines: FooterLines;
|
|
136
|
-
readonly footerItems: readonly FooterItem[];
|
|
137
156
|
readonly subagentResultDisplay: DetailDisplay;
|
|
138
157
|
readonly bashToolDisplay: DetailDisplay;
|
|
139
158
|
readonly fileMutationDisplay: DetailDisplay;
|
|
@@ -164,8 +183,7 @@ export const DEFAULT_SETUP_CONFIG: MyPiSetupConfig = {
|
|
|
164
183
|
customFooter: true,
|
|
165
184
|
footerStyle: DEFAULT_FOOTER_STYLE,
|
|
166
185
|
footerLines: DEFAULT_FOOTER_LINES,
|
|
167
|
-
|
|
168
|
-
subagentResultDisplay: "full",
|
|
186
|
+
subagentResultDisplay: "compact",
|
|
169
187
|
bashToolDisplay: "compact",
|
|
170
188
|
fileMutationDisplay: "compact",
|
|
171
189
|
},
|
|
@@ -256,13 +274,13 @@ export function normalizeFooterLines(value: unknown): FooterLines {
|
|
|
256
274
|
return lines.length > 0 ? lines : DEFAULT_FOOTER_LINES;
|
|
257
275
|
}
|
|
258
276
|
|
|
259
|
-
/** Map a flat item list onto the
|
|
277
|
+
/** Map a flat item list onto the stable one-line skeleton (legacy compat). */
|
|
260
278
|
export function footerLinesFromItems(
|
|
261
279
|
items: readonly FooterItem[],
|
|
262
280
|
): FooterLines {
|
|
263
281
|
const selected = new Set(items);
|
|
264
282
|
return normalizeFooterLines(
|
|
265
|
-
|
|
283
|
+
FLAT_FOOTER_ITEM_LINES.map((line) =>
|
|
266
284
|
line.filter((item) => item === "flex" || selected.has(item)),
|
|
267
285
|
),
|
|
268
286
|
);
|
|
@@ -330,7 +348,6 @@ export function applyFooterConfig(
|
|
|
330
348
|
return {
|
|
331
349
|
footerStyle: style,
|
|
332
350
|
footerLines: normalized,
|
|
333
|
-
footerItems: flattenFooterItems(normalized),
|
|
334
351
|
};
|
|
335
352
|
}
|
|
336
353
|
|
|
@@ -340,19 +357,32 @@ function parseFooterItems(value: unknown): readonly FooterItem[] {
|
|
|
340
357
|
return items.length > 0 ? items : DEFAULT_FOOTER_ITEMS;
|
|
341
358
|
}
|
|
342
359
|
|
|
360
|
+
function sameFooterLines(left: FooterLines, right: FooterLines) {
|
|
361
|
+
return (
|
|
362
|
+
left.length === right.length &&
|
|
363
|
+
left.every(
|
|
364
|
+
(line, lineIndex) =>
|
|
365
|
+
line.length === right[lineIndex]?.length &&
|
|
366
|
+
line.every((item, itemIndex) => item === right[lineIndex]?.[itemIndex]),
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
343
371
|
function parseUiFooter(
|
|
344
372
|
ui: Record<string, unknown>,
|
|
345
|
-
): Pick<MyPiSetupConfig["ui"], "footerStyle" | "footerLines"
|
|
373
|
+
): Pick<MyPiSetupConfig["ui"], "footerStyle" | "footerLines"> {
|
|
346
374
|
const style = isFooterStyle(ui.footerStyle)
|
|
347
375
|
? ui.footerStyle
|
|
348
376
|
: DEFAULT_FOOTER_STYLE;
|
|
349
377
|
|
|
350
378
|
if (ui.footerLines !== undefined) {
|
|
351
|
-
const
|
|
379
|
+
const normalized = normalizeFooterLines(ui.footerLines);
|
|
380
|
+
const lines = sameFooterLines(normalized, LEGACY_DEFAULT_FOOTER_LINES)
|
|
381
|
+
? DEFAULT_FOOTER_LINES
|
|
382
|
+
: normalized;
|
|
352
383
|
return {
|
|
353
384
|
footerStyle: style,
|
|
354
385
|
footerLines: lines,
|
|
355
|
-
footerItems: flattenFooterItems(lines),
|
|
356
386
|
};
|
|
357
387
|
}
|
|
358
388
|
|
|
@@ -363,14 +393,12 @@ function parseUiFooter(
|
|
|
363
393
|
return {
|
|
364
394
|
footerStyle: style,
|
|
365
395
|
footerLines: lines,
|
|
366
|
-
footerItems: flattenFooterItems(lines),
|
|
367
396
|
};
|
|
368
397
|
}
|
|
369
398
|
|
|
370
399
|
return {
|
|
371
400
|
footerStyle: style,
|
|
372
401
|
footerLines: DEFAULT_FOOTER_LINES,
|
|
373
|
-
footerItems: DEFAULT_FOOTER_ITEMS,
|
|
374
402
|
};
|
|
375
403
|
}
|
|
376
404
|
|
|
@@ -465,7 +493,7 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
|
465
493
|
ui.subagentResultDisplay as DetailDisplay,
|
|
466
494
|
)
|
|
467
495
|
? (ui.subagentResultDisplay as DetailDisplay)
|
|
468
|
-
: "
|
|
496
|
+
: "compact",
|
|
469
497
|
bashToolDisplay: DETAIL_DISPLAYS.includes(
|
|
470
498
|
ui.bashToolDisplay as DetailDisplay,
|
|
471
499
|
)
|
|
@@ -485,9 +513,9 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
|
485
513
|
/** An empty or invalid value disables the post-edit command (the safe default). */
|
|
486
514
|
function parsePostEditCommand(value: unknown) {
|
|
487
515
|
if (!isRecord(value)) return "";
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
516
|
+
if (typeof value.command !== "string") return "";
|
|
517
|
+
const command = value.command.trim();
|
|
518
|
+
return command.length <= POST_EDIT_COMMAND_MAX_CHARS ? command : "";
|
|
491
519
|
}
|
|
492
520
|
|
|
493
521
|
export function hasSavedSetupConfig() {
|
|
@@ -542,6 +570,9 @@ function replacedFields(
|
|
|
542
570
|
) {
|
|
543
571
|
paths.push("summaries → suggestions");
|
|
544
572
|
}
|
|
573
|
+
if (path === "ui" && "footerItems" in raw && "footerLines" in normalized) {
|
|
574
|
+
paths.push("ui.footerItems");
|
|
575
|
+
}
|
|
545
576
|
for (const [key, value] of Object.entries(normalized)) {
|
|
546
577
|
if (!(key in raw)) continue;
|
|
547
578
|
const here = path ? `${path}.${key}` : key;
|
|
@@ -608,24 +639,55 @@ const sameOwner = (left: LockOwner, right: LockOwner) =>
|
|
|
608
639
|
const sameFile = (left: Stats, right: Stats) =>
|
|
609
640
|
left.dev === right.dev && left.ino === right.ino;
|
|
610
641
|
|
|
642
|
+
const parsePowerShellProcessStartedAt = (stdout: string) => {
|
|
643
|
+
const startedAt = Number(stdout.trim());
|
|
644
|
+
return isPositiveInteger(startedAt) ? startedAt : undefined;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const parsePsProcessStartedAt = (stdout: string) => {
|
|
648
|
+
const startedAt = Date.parse(stdout.trim());
|
|
649
|
+
return Number.isFinite(startedAt) ? startedAt : undefined;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
export function processStartedAtQuery(
|
|
653
|
+
pid: number,
|
|
654
|
+
platform = process.platform,
|
|
655
|
+
) {
|
|
656
|
+
if (platform === "win32") {
|
|
657
|
+
const script = [
|
|
658
|
+
"$ErrorActionPreference='Stop'",
|
|
659
|
+
`$p=Get-Process -Id ${pid}`,
|
|
660
|
+
"[Console]::Out.Write(([DateTimeOffset]$p.StartTime).ToUnixTimeMilliseconds())",
|
|
661
|
+
].join("; ");
|
|
662
|
+
return {
|
|
663
|
+
command: "powershell.exe",
|
|
664
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", script],
|
|
665
|
+
env: undefined,
|
|
666
|
+
parseOutput: parsePowerShellProcessStartedAt,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
command: "ps",
|
|
671
|
+
args: ["-o", "lstart=", "-p", String(pid)],
|
|
672
|
+
env: { LC_ALL: "C" },
|
|
673
|
+
parseOutput: parsePsProcessStartedAt,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
611
677
|
const queryProcessStartedAt = (pid: number) =>
|
|
612
678
|
new Promise<number | undefined>((resolve) => {
|
|
613
|
-
|
|
614
|
-
resolve(undefined);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
679
|
+
const query = processStartedAtQuery(pid);
|
|
617
680
|
try {
|
|
618
681
|
execFile(
|
|
619
|
-
|
|
620
|
-
|
|
682
|
+
query.command,
|
|
683
|
+
query.args,
|
|
621
684
|
{
|
|
622
685
|
encoding: "utf8",
|
|
623
|
-
env: { ...process.env,
|
|
686
|
+
env: query.env ? { ...process.env, ...query.env } : process.env,
|
|
624
687
|
timeout: 1_000,
|
|
625
688
|
},
|
|
626
689
|
(error, stdout) => {
|
|
627
|
-
|
|
628
|
-
resolve(Number.isFinite(startedAt) ? startedAt : undefined);
|
|
690
|
+
resolve(error ? undefined : query.parseOutput(stdout));
|
|
629
691
|
},
|
|
630
692
|
);
|
|
631
693
|
} catch {
|
|
@@ -919,13 +981,15 @@ async function withSetupConfigLock<A>(action: () => Promise<A>) {
|
|
|
919
981
|
}
|
|
920
982
|
|
|
921
983
|
async function writeSetupConfig(config: MyPiSetupConfig) {
|
|
984
|
+
const canonical = parseSetupConfig(config);
|
|
922
985
|
const tempPath = `${SETUP_CONFIG_PATH}.${process.pid}.${randomUUID()}.tmp`;
|
|
923
986
|
try {
|
|
924
|
-
await writeFile(tempPath, `${JSON.stringify(
|
|
987
|
+
await writeFile(tempPath, `${JSON.stringify(canonical, null, 2)}\n`, {
|
|
925
988
|
encoding: "utf8",
|
|
926
989
|
mode: 0o600,
|
|
927
990
|
});
|
|
928
991
|
await rename(tempPath, SETUP_CONFIG_PATH);
|
|
992
|
+
return canonical;
|
|
929
993
|
} catch (error) {
|
|
930
994
|
await unlink(tempPath).catch(() => undefined);
|
|
931
995
|
throw error;
|
|
@@ -939,10 +1003,7 @@ export async function saveSetupConfig(config: MyPiSetupConfig) {
|
|
|
939
1003
|
});
|
|
940
1004
|
}
|
|
941
1005
|
|
|
942
|
-
export function formatSetupConfig(
|
|
943
|
-
config = loadSetupConfig(),
|
|
944
|
-
integrationLines: readonly string[] = [],
|
|
945
|
-
) {
|
|
1006
|
+
export function formatSetupConfig(config = loadSetupConfig()) {
|
|
946
1007
|
const suggestionModel = config.suggestions.model;
|
|
947
1008
|
const suggestions =
|
|
948
1009
|
!config.suggestions.enabled || !suggestionModel
|
|
@@ -957,11 +1018,10 @@ export function formatSetupConfig(
|
|
|
957
1018
|
`Workflows: ${config.workflows.concurrency} concurrent agents · ${config.workflows.maxAgentCalls} total calls`,
|
|
958
1019
|
`UI: large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
|
|
959
1020
|
`Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact status summary (Ctrl+O expands full output)"}`,
|
|
960
|
-
`Bash operations: ${config.ui.bashToolDisplay === "full" ? "expanded by default" : "
|
|
961
|
-
`Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "
|
|
962
|
-
`Post-edit command: ${config.postEdit.command ?
|
|
1021
|
+
`Bash operations: ${config.ui.bashToolDisplay === "full" ? "expanded by default" : "one-line activity summary (Ctrl+O restores native evidence)"}`,
|
|
1022
|
+
`Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "one-line activity summary (Ctrl+O restores native evidence)"}`,
|
|
1023
|
+
`Post-edit command: ${config.postEdit.command ? "configured" : "off"}`,
|
|
963
1024
|
`Agent role models (Subagents + Workflows): ${SUBAGENT_ROLE_NAMES.map((role) => `${role} ${config.subagents.roleModels[role] ? `${config.subagents.roleModels[role].provider}/${config.subagents.roleModels[role].model}` : "inherit"}`).join(" · ")}`,
|
|
964
|
-
...integrationLines,
|
|
965
1025
|
].join("\n");
|
|
966
1026
|
}
|
|
967
1027
|
|
|
@@ -979,8 +1039,7 @@ export async function updateSetupConfig(
|
|
|
979
1039
|
return withSetupConfigLock(async () => {
|
|
980
1040
|
const raw = readDocumentForWrite();
|
|
981
1041
|
const current = parseSetupConfig(raw);
|
|
982
|
-
const config = mutate(current);
|
|
983
|
-
await writeSetupConfig(config);
|
|
1042
|
+
const config = await writeSetupConfig(mutate(current));
|
|
984
1043
|
return { config, replaced: replacedFields(raw, current) };
|
|
985
1044
|
});
|
|
986
1045
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export const OPENPI_SETUP_EPISODE_CHANNEL = "openpi:setup-episode";
|
|
3
3
|
|
|
4
4
|
export interface OpenPiSetupEpisodeState {
|
|
5
|
-
/** True
|
|
5
|
+
/** True only while the owned setup writer is active for a delivered request. */
|
|
6
6
|
readonly active: boolean;
|
|
7
7
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One braille spinner for every running-state indicator in the package:
|
|
3
|
+
* transcripts, takeover and dashboard headers, and the below-editor strips all
|
|
4
|
+
* advance on the same cadence so concurrent views animate in step.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const SPINNER_FRAMES = [
|
|
8
|
+
"⠋",
|
|
9
|
+
"⠙",
|
|
10
|
+
"⠹",
|
|
11
|
+
"⠸",
|
|
12
|
+
"⠼",
|
|
13
|
+
"⠴",
|
|
14
|
+
"⠦",
|
|
15
|
+
"⠧",
|
|
16
|
+
"⠇",
|
|
17
|
+
"⠏",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Frame cadence, shared with the dashboard and takeover headers. */
|
|
21
|
+
export const SPINNER_INTERVAL_MS = 120;
|
|
22
|
+
|
|
23
|
+
export function spinnerFrame(now: number) {
|
|
24
|
+
const frame = Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
|
|
25
|
+
return SPINNER_FRAMES[
|
|
26
|
+
(frame + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
|
|
27
|
+
];
|
|
28
|
+
}
|
|
@@ -1,35 +1,122 @@
|
|
|
1
|
-
// OSC may end in BEL or ST. DCS/SOS/PM/APC end only in ST; treating BEL as
|
|
2
|
-
// their terminator would expose the rest of a still-hidden payload. Both
|
|
3
|
-
// patterns consume an unterminated string through the bounded input's end.
|
|
4
|
-
// eslint-disable-next-line no-control-regex
|
|
5
|
-
const OSC_PATTERN =
|
|
6
|
-
/(?:\u001b\]|\u009d)(?:[^\u0007\u001b\u009c]|\u001b(?!\\))*(?:\u0007|\u001b\\|\u009c|$)/g;
|
|
7
|
-
// eslint-disable-next-line no-control-regex
|
|
8
|
-
const ST_STRING_PATTERN =
|
|
9
|
-
/(?:\u001b[PX^_]|[\u0090\u0098\u009e\u009f])(?:[^\u001b\u009c]|\u001b(?!\\))*(?:\u001b\\|\u009c|$)/g;
|
|
10
|
-
// eslint-disable-next-line no-control-regex
|
|
11
|
-
const CSI_PATTERN = /(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]/g;
|
|
12
|
-
// Remaining two-byte/charset escape forms (for example ESC ( 0).
|
|
13
|
-
// eslint-disable-next-line no-control-regex
|
|
14
|
-
const ESCAPE_PATTERN = /\u001b(?:[()][0-2A-Z]|[ -/]*[@-~])/g;
|
|
15
1
|
// Invisible Unicode format controls can reorder or disguise untrusted text.
|
|
16
2
|
// Keep only ZWNJ/ZWJ (U+200C/U+200D), which carry legitimate shaping and emoji
|
|
17
3
|
// semantics; variation selectors are marks rather than Cf and remain intact.
|
|
18
|
-
const
|
|
4
|
+
const UNSAFE_FORMAT_CHARACTER = /\p{Cf}/u;
|
|
19
5
|
// eslint-disable-next-line no-control-regex
|
|
20
6
|
const TERMINAL_CONTROL_PATTERN =
|
|
21
7
|
/[\u0000-\u001f\u007f-\u009f]|(?:(?![\u200c\u200d])\p{Cf})/u;
|
|
22
8
|
|
|
9
|
+
type SanitizerState =
|
|
10
|
+
| "text"
|
|
11
|
+
| "escape"
|
|
12
|
+
| "escape_intermediate"
|
|
13
|
+
| "csi"
|
|
14
|
+
| "osc"
|
|
15
|
+
| "st_string";
|
|
16
|
+
|
|
17
|
+
/** Stateful terminal-text projection for streams whose retained head can move. */
|
|
18
|
+
export class TerminalTextSanitizer {
|
|
19
|
+
private state: SanitizerState = "text";
|
|
20
|
+
private stringEscape = false;
|
|
21
|
+
|
|
22
|
+
push(text: string) {
|
|
23
|
+
let safe = "";
|
|
24
|
+
for (const character of text) {
|
|
25
|
+
const code = character.codePointAt(0)!;
|
|
26
|
+
|
|
27
|
+
if (this.state === "osc" || this.state === "st_string") {
|
|
28
|
+
if (character === "\u009c") {
|
|
29
|
+
this.state = "text";
|
|
30
|
+
this.stringEscape = false;
|
|
31
|
+
} else if (this.state === "osc" && character === "\u0007") {
|
|
32
|
+
this.state = "text";
|
|
33
|
+
this.stringEscape = false;
|
|
34
|
+
} else if (this.stringEscape && character === "\\") {
|
|
35
|
+
this.state = "text";
|
|
36
|
+
this.stringEscape = false;
|
|
37
|
+
} else {
|
|
38
|
+
this.stringEscape = character === "\u001b";
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.state === "csi") {
|
|
44
|
+
if (code >= 0x40 && code <= 0x7e) this.state = "text";
|
|
45
|
+
else if (code < 0x20 || code > 0x3f) {
|
|
46
|
+
this.state = "text";
|
|
47
|
+
safe += this.safeCharacter(character, code);
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.state === "escape" || this.state === "escape_intermediate") {
|
|
53
|
+
if (this.state === "escape") {
|
|
54
|
+
if (character === "[") {
|
|
55
|
+
this.state = "csi";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (character === "]") {
|
|
59
|
+
this.state = "osc";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
character === "P" ||
|
|
64
|
+
character === "X" ||
|
|
65
|
+
character === "^" ||
|
|
66
|
+
character === "_"
|
|
67
|
+
) {
|
|
68
|
+
this.state = "st_string";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (code >= 0x20 && code <= 0x2f) {
|
|
73
|
+
this.state = "escape_intermediate";
|
|
74
|
+
} else if (code >= 0x30 && code <= 0x7e) {
|
|
75
|
+
this.state = "text";
|
|
76
|
+
} else {
|
|
77
|
+
this.state = "text";
|
|
78
|
+
safe += this.safeCharacter(character, code);
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (character === "\u001b") {
|
|
84
|
+
this.state = "escape";
|
|
85
|
+
} else if (character === "\u009b") {
|
|
86
|
+
this.state = "csi";
|
|
87
|
+
} else if (character === "\u009d") {
|
|
88
|
+
this.state = "osc";
|
|
89
|
+
} else if (
|
|
90
|
+
character === "\u0090" ||
|
|
91
|
+
character === "\u0098" ||
|
|
92
|
+
character === "\u009e" ||
|
|
93
|
+
character === "\u009f"
|
|
94
|
+
) {
|
|
95
|
+
this.state = "st_string";
|
|
96
|
+
} else {
|
|
97
|
+
safe += this.safeCharacter(character, code);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return safe;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private safeCharacter(character: string, code: number) {
|
|
104
|
+
if (character === "\t") return " ";
|
|
105
|
+
if (
|
|
106
|
+
character === "\n" ||
|
|
107
|
+
character === "\u200c" ||
|
|
108
|
+
character === "\u200d"
|
|
109
|
+
) {
|
|
110
|
+
return character;
|
|
111
|
+
}
|
|
112
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) return "";
|
|
113
|
+
return UNSAFE_FORMAT_CHARACTER.test(character) ? "" : character;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
23
117
|
/** Strip terminal control sequences and direction-spoofing format controls. */
|
|
24
118
|
export function sanitizeTerminalText(text: string) {
|
|
25
|
-
return text
|
|
26
|
-
.replace(OSC_PATTERN, "")
|
|
27
|
-
.replace(ST_STRING_PATTERN, "")
|
|
28
|
-
.replace(CSI_PATTERN, "")
|
|
29
|
-
.replace(ESCAPE_PATTERN, "")
|
|
30
|
-
.replace(UNSAFE_FORMAT_PATTERN, "")
|
|
31
|
-
.replaceAll("\t", " ")
|
|
32
|
-
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
119
|
+
return new TerminalTextSanitizer().push(text);
|
|
33
120
|
}
|
|
34
121
|
|
|
35
122
|
/** Reject raw terminal or direction-spoofing controls in display patterns. */
|