@tt-a1i/openpi 0.4.0 → 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 +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- 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 +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- 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/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- 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 +153 -86
- 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 +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- 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/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- 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 +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- 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/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- 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/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -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 +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- 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 -303
|
@@ -72,6 +72,22 @@ export const DEFAULT_FOOTER_LINES: FooterLines = [
|
|
|
72
72
|
["model", "context", "flex", "git", "pr", "cwd"],
|
|
73
73
|
];
|
|
74
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
|
+
|
|
75
91
|
/** The persisted canonical default before model/context moved to the left. */
|
|
76
92
|
const LEGACY_DEFAULT_FOOTER_LINES: FooterLines = [
|
|
77
93
|
["cwd", "git", "pr", "flex", "model", "context"],
|
|
@@ -136,9 +152,7 @@ export interface MyPiSetupConfig {
|
|
|
136
152
|
readonly showHeader: boolean;
|
|
137
153
|
readonly customFooter: boolean;
|
|
138
154
|
readonly footerStyle: FooterStyle;
|
|
139
|
-
/** Canonical footer layout. `footerItems` is always derived from this. */
|
|
140
155
|
readonly footerLines: FooterLines;
|
|
141
|
-
readonly footerItems: readonly FooterItem[];
|
|
142
156
|
readonly subagentResultDisplay: DetailDisplay;
|
|
143
157
|
readonly bashToolDisplay: DetailDisplay;
|
|
144
158
|
readonly fileMutationDisplay: DetailDisplay;
|
|
@@ -169,8 +183,7 @@ export const DEFAULT_SETUP_CONFIG: MyPiSetupConfig = {
|
|
|
169
183
|
customFooter: true,
|
|
170
184
|
footerStyle: DEFAULT_FOOTER_STYLE,
|
|
171
185
|
footerLines: DEFAULT_FOOTER_LINES,
|
|
172
|
-
|
|
173
|
-
subagentResultDisplay: "full",
|
|
186
|
+
subagentResultDisplay: "compact",
|
|
174
187
|
bashToolDisplay: "compact",
|
|
175
188
|
fileMutationDisplay: "compact",
|
|
176
189
|
},
|
|
@@ -261,13 +274,13 @@ export function normalizeFooterLines(value: unknown): FooterLines {
|
|
|
261
274
|
return lines.length > 0 ? lines : DEFAULT_FOOTER_LINES;
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
/** Map a flat item list onto the
|
|
277
|
+
/** Map a flat item list onto the stable one-line skeleton (legacy compat). */
|
|
265
278
|
export function footerLinesFromItems(
|
|
266
279
|
items: readonly FooterItem[],
|
|
267
280
|
): FooterLines {
|
|
268
281
|
const selected = new Set(items);
|
|
269
282
|
return normalizeFooterLines(
|
|
270
|
-
|
|
283
|
+
FLAT_FOOTER_ITEM_LINES.map((line) =>
|
|
271
284
|
line.filter((item) => item === "flex" || selected.has(item)),
|
|
272
285
|
),
|
|
273
286
|
);
|
|
@@ -335,7 +348,6 @@ export function applyFooterConfig(
|
|
|
335
348
|
return {
|
|
336
349
|
footerStyle: style,
|
|
337
350
|
footerLines: normalized,
|
|
338
|
-
footerItems: flattenFooterItems(normalized),
|
|
339
351
|
};
|
|
340
352
|
}
|
|
341
353
|
|
|
@@ -358,7 +370,7 @@ function sameFooterLines(left: FooterLines, right: FooterLines) {
|
|
|
358
370
|
|
|
359
371
|
function parseUiFooter(
|
|
360
372
|
ui: Record<string, unknown>,
|
|
361
|
-
): Pick<MyPiSetupConfig["ui"], "footerStyle" | "footerLines"
|
|
373
|
+
): Pick<MyPiSetupConfig["ui"], "footerStyle" | "footerLines"> {
|
|
362
374
|
const style = isFooterStyle(ui.footerStyle)
|
|
363
375
|
? ui.footerStyle
|
|
364
376
|
: DEFAULT_FOOTER_STYLE;
|
|
@@ -371,7 +383,6 @@ function parseUiFooter(
|
|
|
371
383
|
return {
|
|
372
384
|
footerStyle: style,
|
|
373
385
|
footerLines: lines,
|
|
374
|
-
footerItems: flattenFooterItems(lines),
|
|
375
386
|
};
|
|
376
387
|
}
|
|
377
388
|
|
|
@@ -382,14 +393,12 @@ function parseUiFooter(
|
|
|
382
393
|
return {
|
|
383
394
|
footerStyle: style,
|
|
384
395
|
footerLines: lines,
|
|
385
|
-
footerItems: flattenFooterItems(lines),
|
|
386
396
|
};
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
return {
|
|
390
400
|
footerStyle: style,
|
|
391
401
|
footerLines: DEFAULT_FOOTER_LINES,
|
|
392
|
-
footerItems: DEFAULT_FOOTER_ITEMS,
|
|
393
402
|
};
|
|
394
403
|
}
|
|
395
404
|
|
|
@@ -484,7 +493,7 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
|
484
493
|
ui.subagentResultDisplay as DetailDisplay,
|
|
485
494
|
)
|
|
486
495
|
? (ui.subagentResultDisplay as DetailDisplay)
|
|
487
|
-
: "
|
|
496
|
+
: "compact",
|
|
488
497
|
bashToolDisplay: DETAIL_DISPLAYS.includes(
|
|
489
498
|
ui.bashToolDisplay as DetailDisplay,
|
|
490
499
|
)
|
|
@@ -504,9 +513,9 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
|
504
513
|
/** An empty or invalid value disables the post-edit command (the safe default). */
|
|
505
514
|
function parsePostEditCommand(value: unknown) {
|
|
506
515
|
if (!isRecord(value)) return "";
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
516
|
+
if (typeof value.command !== "string") return "";
|
|
517
|
+
const command = value.command.trim();
|
|
518
|
+
return command.length <= POST_EDIT_COMMAND_MAX_CHARS ? command : "";
|
|
510
519
|
}
|
|
511
520
|
|
|
512
521
|
export function hasSavedSetupConfig() {
|
|
@@ -561,6 +570,9 @@ function replacedFields(
|
|
|
561
570
|
) {
|
|
562
571
|
paths.push("summaries → suggestions");
|
|
563
572
|
}
|
|
573
|
+
if (path === "ui" && "footerItems" in raw && "footerLines" in normalized) {
|
|
574
|
+
paths.push("ui.footerItems");
|
|
575
|
+
}
|
|
564
576
|
for (const [key, value] of Object.entries(normalized)) {
|
|
565
577
|
if (!(key in raw)) continue;
|
|
566
578
|
const here = path ? `${path}.${key}` : key;
|
|
@@ -627,24 +639,55 @@ const sameOwner = (left: LockOwner, right: LockOwner) =>
|
|
|
627
639
|
const sameFile = (left: Stats, right: Stats) =>
|
|
628
640
|
left.dev === right.dev && left.ino === right.ino;
|
|
629
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
|
+
|
|
630
677
|
const queryProcessStartedAt = (pid: number) =>
|
|
631
678
|
new Promise<number | undefined>((resolve) => {
|
|
632
|
-
|
|
633
|
-
resolve(undefined);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
679
|
+
const query = processStartedAtQuery(pid);
|
|
636
680
|
try {
|
|
637
681
|
execFile(
|
|
638
|
-
|
|
639
|
-
|
|
682
|
+
query.command,
|
|
683
|
+
query.args,
|
|
640
684
|
{
|
|
641
685
|
encoding: "utf8",
|
|
642
|
-
env: { ...process.env,
|
|
686
|
+
env: query.env ? { ...process.env, ...query.env } : process.env,
|
|
643
687
|
timeout: 1_000,
|
|
644
688
|
},
|
|
645
689
|
(error, stdout) => {
|
|
646
|
-
|
|
647
|
-
resolve(Number.isFinite(startedAt) ? startedAt : undefined);
|
|
690
|
+
resolve(error ? undefined : query.parseOutput(stdout));
|
|
648
691
|
},
|
|
649
692
|
);
|
|
650
693
|
} catch {
|
|
@@ -938,13 +981,15 @@ async function withSetupConfigLock<A>(action: () => Promise<A>) {
|
|
|
938
981
|
}
|
|
939
982
|
|
|
940
983
|
async function writeSetupConfig(config: MyPiSetupConfig) {
|
|
984
|
+
const canonical = parseSetupConfig(config);
|
|
941
985
|
const tempPath = `${SETUP_CONFIG_PATH}.${process.pid}.${randomUUID()}.tmp`;
|
|
942
986
|
try {
|
|
943
|
-
await writeFile(tempPath, `${JSON.stringify(
|
|
987
|
+
await writeFile(tempPath, `${JSON.stringify(canonical, null, 2)}\n`, {
|
|
944
988
|
encoding: "utf8",
|
|
945
989
|
mode: 0o600,
|
|
946
990
|
});
|
|
947
991
|
await rename(tempPath, SETUP_CONFIG_PATH);
|
|
992
|
+
return canonical;
|
|
948
993
|
} catch (error) {
|
|
949
994
|
await unlink(tempPath).catch(() => undefined);
|
|
950
995
|
throw error;
|
|
@@ -958,10 +1003,7 @@ export async function saveSetupConfig(config: MyPiSetupConfig) {
|
|
|
958
1003
|
});
|
|
959
1004
|
}
|
|
960
1005
|
|
|
961
|
-
export function formatSetupConfig(
|
|
962
|
-
config = loadSetupConfig(),
|
|
963
|
-
integrationLines: readonly string[] = [],
|
|
964
|
-
) {
|
|
1006
|
+
export function formatSetupConfig(config = loadSetupConfig()) {
|
|
965
1007
|
const suggestionModel = config.suggestions.model;
|
|
966
1008
|
const suggestions =
|
|
967
1009
|
!config.suggestions.enabled || !suggestionModel
|
|
@@ -978,9 +1020,8 @@ export function formatSetupConfig(
|
|
|
978
1020
|
`Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact status summary (Ctrl+O expands full output)"}`,
|
|
979
1021
|
`Bash operations: ${config.ui.bashToolDisplay === "full" ? "expanded by default" : "one-line activity summary (Ctrl+O restores native evidence)"}`,
|
|
980
1022
|
`Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "one-line activity summary (Ctrl+O restores native evidence)"}`,
|
|
981
|
-
`Post-edit command: ${config.postEdit.command ?
|
|
1023
|
+
`Post-edit command: ${config.postEdit.command ? "configured" : "off"}`,
|
|
982
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(" · ")}`,
|
|
983
|
-
...integrationLines,
|
|
984
1025
|
].join("\n");
|
|
985
1026
|
}
|
|
986
1027
|
|
|
@@ -998,8 +1039,7 @@ export async function updateSetupConfig(
|
|
|
998
1039
|
return withSetupConfigLock(async () => {
|
|
999
1040
|
const raw = readDocumentForWrite();
|
|
1000
1041
|
const current = parseSetupConfig(raw);
|
|
1001
|
-
const config = mutate(current);
|
|
1002
|
-
await writeSetupConfig(config);
|
|
1042
|
+
const config = await writeSetupConfig(mutate(current));
|
|
1003
1043
|
return { config, replaced: replacedFields(raw, current) };
|
|
1004
1044
|
});
|
|
1005
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
|
}
|
|
@@ -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. */
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
formatSize,
|
|
3
|
+
truncateHead,
|
|
4
|
+
truncateTail,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
|
|
3
7
|
const HEAD_SHARE = 0.75;
|
|
8
|
+
const OMISSION_MARKER = "[... middle omitted ...]";
|
|
9
|
+
// Three framing blanks, the omission marker, and the footer.
|
|
10
|
+
const STATIC_PROJECTION_OVERHEAD_LINES = 5;
|
|
4
11
|
|
|
5
12
|
function utf8Prefix(content: string, maxBytes: number) {
|
|
6
13
|
const bytes = Buffer.from(content, "utf8");
|
|
@@ -14,16 +21,46 @@ function utf8Prefix(content: string, maxBytes: number) {
|
|
|
14
21
|
return "";
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
function boundedHead(content: string, maxBytes: number, maxLines: number) {
|
|
25
|
+
const result = truncateHead(content, { maxBytes, maxLines });
|
|
26
|
+
if (result.content || !result.firstLineExceedsLimit) return result.content;
|
|
27
|
+
return utf8Prefix(content, maxBytes);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compactProjection(
|
|
31
|
+
content: string,
|
|
32
|
+
maxBytes: number,
|
|
33
|
+
maxLines: number,
|
|
34
|
+
) {
|
|
35
|
+
const marker =
|
|
36
|
+
maxBytes >= OMISSION_MARKER.length
|
|
37
|
+
? OMISSION_MARKER
|
|
38
|
+
: ".".repeat(Math.min(3, maxBytes));
|
|
39
|
+
if (!marker || maxLines <= 0) return "";
|
|
40
|
+
if (maxLines === 1) return marker;
|
|
41
|
+
|
|
42
|
+
const bodyLineBudget = maxLines - 1;
|
|
43
|
+
const keepTail = bodyLineBudget >= 2;
|
|
44
|
+
const separatorBytes = keepTail ? 2 : 1;
|
|
45
|
+
const bodyByteBudget = maxBytes - marker.length - separatorBytes;
|
|
46
|
+
if (bodyByteBudget <= 0 || (keepTail && bodyByteBudget < 2)) return marker;
|
|
47
|
+
|
|
48
|
+
const headLines = keepTail
|
|
49
|
+
? Math.max(1, Math.floor(bodyLineBudget * HEAD_SHARE))
|
|
50
|
+
: bodyLineBudget;
|
|
51
|
+
const tailLines = keepTail ? Math.max(1, bodyLineBudget - headLines) : 0;
|
|
52
|
+
const headBytes = keepTail
|
|
53
|
+
? Math.max(1, Math.floor(bodyByteBudget * HEAD_SHARE))
|
|
54
|
+
: bodyByteBudget;
|
|
55
|
+
const tailBytes = keepTail ? Math.max(1, bodyByteBudget - headBytes) : 0;
|
|
56
|
+
const head = boundedHead(content, headBytes, headLines);
|
|
57
|
+
const tail = keepTail
|
|
58
|
+
? truncateTail(content, {
|
|
59
|
+
maxBytes: tailBytes,
|
|
60
|
+
maxLines: tailLines,
|
|
61
|
+
}).content
|
|
62
|
+
: "";
|
|
63
|
+
return [head, marker, tail].filter(Boolean).join("\n");
|
|
27
64
|
}
|
|
28
65
|
|
|
29
66
|
/** Keep decision context at the start and verdict/evidence at the end. */
|
|
@@ -31,26 +68,46 @@ export function projectText(
|
|
|
31
68
|
content: string,
|
|
32
69
|
options: { maxBytes: number; maxLines: number; recovery: string },
|
|
33
70
|
) {
|
|
71
|
+
if (options.maxBytes <= 0 || options.maxLines <= 0) return "";
|
|
72
|
+
|
|
34
73
|
const probe = truncateHead(content, {
|
|
35
74
|
maxBytes: options.maxBytes,
|
|
36
75
|
maxLines: options.maxLines,
|
|
37
76
|
});
|
|
38
77
|
if (!probe.truncated) return content;
|
|
39
78
|
|
|
79
|
+
const recoveryNewlines = options.recovery.split("\n").length - 1;
|
|
80
|
+
const bodyLineBudget =
|
|
81
|
+
options.maxLines - STATIC_PROJECTION_OVERHEAD_LINES - recoveryNewlines;
|
|
82
|
+
if (bodyLineBudget < 2) {
|
|
83
|
+
return compactProjection(content, options.maxBytes, options.maxLines);
|
|
84
|
+
}
|
|
85
|
+
const headLines = Math.max(1, Math.floor(bodyLineBudget * HEAD_SHARE));
|
|
86
|
+
const tailLines = Math.max(1, bodyLineBudget - headLines);
|
|
87
|
+
|
|
40
88
|
let bodyBudget = options.maxBytes;
|
|
41
89
|
let projected = "";
|
|
42
90
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
43
91
|
const headBytes = Math.max(1, Math.floor(bodyBudget * HEAD_SHARE));
|
|
44
92
|
const tailBytes = Math.max(1, bodyBudget - headBytes);
|
|
45
|
-
const head =
|
|
46
|
-
const tail =
|
|
93
|
+
const head = boundedHead(content, headBytes, headLines);
|
|
94
|
+
const tail = truncateTail(content, {
|
|
95
|
+
maxBytes: tailBytes,
|
|
96
|
+
maxLines: tailLines,
|
|
97
|
+
}).content;
|
|
47
98
|
const shown =
|
|
48
99
|
Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
|
|
49
100
|
const footer = `[Projection bounded: showing ${formatSize(shown)} of ${formatSize(probe.totalBytes)} across the head and tail. ${options.recovery}]`;
|
|
50
|
-
projected = `${head}\n\n
|
|
101
|
+
projected = `${head}\n\n${OMISSION_MARKER}\n\n${tail}\n\n${footer}`;
|
|
51
102
|
const overflow = Buffer.byteLength(projected, "utf8") - options.maxBytes;
|
|
52
103
|
if (overflow <= 0 || bodyBudget <= overflow + 2) break;
|
|
53
104
|
bodyBudget -= overflow;
|
|
54
105
|
}
|
|
55
|
-
|
|
106
|
+
const projectedProbe = truncateHead(projected, {
|
|
107
|
+
maxBytes: options.maxBytes,
|
|
108
|
+
maxLines: options.maxLines,
|
|
109
|
+
});
|
|
110
|
+
return projectedProbe.truncated
|
|
111
|
+
? compactProjection(content, options.maxBytes, options.maxLines)
|
|
112
|
+
: projected;
|
|
56
113
|
}
|