@tt-a1i/openpi 0.4.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 +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- 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 +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -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 +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- 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/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -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 +82 -6
- 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 +138 -130
- 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 +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- 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 +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- 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 +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- 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 +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -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
|
@@ -3,8 +3,12 @@ import {
|
|
|
3
3
|
keyHint,
|
|
4
4
|
type Theme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
type Component,
|
|
8
|
+
Markdown,
|
|
9
|
+
truncateToWidth,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
import { sanitizeText } from "../../../shared/agent-transcript.ts";
|
|
8
12
|
|
|
9
13
|
const MAX_STATUS_ROWS = 4;
|
|
10
14
|
|
|
@@ -12,40 +16,116 @@ export interface WaitResultItem {
|
|
|
12
16
|
readonly id: string;
|
|
13
17
|
readonly title?: string;
|
|
14
18
|
readonly status?: string;
|
|
19
|
+
readonly outcome?: "completed" | "failed" | "interrupted";
|
|
20
|
+
readonly worktreeBranch?: string;
|
|
21
|
+
readonly elapsed?: string;
|
|
22
|
+
readonly artifactSaveFailed?: boolean;
|
|
23
|
+
readonly fullResultSaved?: boolean;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export interface WaitResultDetails {
|
|
18
27
|
readonly results?: readonly WaitResultItem[];
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
function singleLine(value: string) {
|
|
31
|
+
return sanitizeText(value).replace(/\s+/gu, " ").trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fixedRows(rows: readonly string[]): Component {
|
|
35
|
+
return {
|
|
36
|
+
render(width) {
|
|
37
|
+
return rows.map((row) => truncateToWidth(row, Math.max(1, width), "…"));
|
|
38
|
+
},
|
|
39
|
+
invalidate() {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function selectStatusRows(results: readonly WaitResultItem[]) {
|
|
44
|
+
const visible = results.slice(0, MAX_STATUS_ROWS);
|
|
45
|
+
if (visible.some((result) => result.status === "error")) {
|
|
46
|
+
return visible;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hiddenFailure = results
|
|
50
|
+
.slice(MAX_STATUS_ROWS)
|
|
51
|
+
.find((result) => result.status === "error");
|
|
52
|
+
return hiddenFailure ? [...visible.slice(0, -1), hiddenFailure] : visible;
|
|
53
|
+
}
|
|
54
|
+
|
|
21
55
|
export function buildWaitResultPreview(
|
|
22
56
|
content: string,
|
|
23
57
|
details: WaitResultDetails | undefined,
|
|
24
58
|
theme: Theme,
|
|
25
59
|
) {
|
|
26
60
|
const results = details?.results ?? [];
|
|
27
|
-
const
|
|
61
|
+
const uncertain = results.filter(
|
|
62
|
+
(result) => result.outcome === "interrupted",
|
|
63
|
+
).length;
|
|
64
|
+
const failed = results.filter(
|
|
65
|
+
(result) => result.status === "error" && result.outcome !== "interrupted",
|
|
66
|
+
).length;
|
|
67
|
+
const artifactFailures = results.filter(
|
|
68
|
+
(result) => result.artifactSaveFailed,
|
|
69
|
+
).length;
|
|
70
|
+
const exceptions = [
|
|
71
|
+
failed > 0 ? theme.fg("error", `${failed} failed`) : "",
|
|
72
|
+
uncertain > 0 ? theme.fg("warning", `${uncertain} uncertain`) : "",
|
|
73
|
+
].filter(Boolean);
|
|
28
74
|
const header =
|
|
29
|
-
theme.fg(
|
|
30
|
-
|
|
31
|
-
|
|
75
|
+
theme.fg(
|
|
76
|
+
failed > 0 || uncertain > 0 ? "warning" : "success",
|
|
77
|
+
failed > 0 || uncertain > 0 ? "!" : "✓",
|
|
78
|
+
) +
|
|
79
|
+
` ${exceptions.length > 0 ? `${exceptions.join(" · ")} · ` : ""}` +
|
|
80
|
+
theme.fg(
|
|
81
|
+
"accent",
|
|
82
|
+
theme.bold(
|
|
83
|
+
`${results.length} subagent${results.length === 1 ? "" : "s"} settled`,
|
|
84
|
+
),
|
|
85
|
+
) +
|
|
86
|
+
(artifactFailures > 0
|
|
87
|
+
? theme.fg(
|
|
88
|
+
"warning",
|
|
89
|
+
` · ${artifactFailures} artifact${artifactFailures === 1 ? "" : "s"} not saved`,
|
|
90
|
+
)
|
|
91
|
+
: "");
|
|
32
92
|
const lines = [header];
|
|
93
|
+
const statusRows = selectStatusRows(results);
|
|
33
94
|
|
|
34
|
-
for (const result of
|
|
35
|
-
const
|
|
95
|
+
for (const result of statusRows) {
|
|
96
|
+
const isUncertain = result.outcome === "interrupted";
|
|
97
|
+
const isFailure = result.status === "error" && !isUncertain;
|
|
36
98
|
const icon = theme.fg(
|
|
37
|
-
isFailure ? "error" : "success",
|
|
38
|
-
isFailure ? "x" : "✓",
|
|
99
|
+
isFailure ? "error" : isUncertain ? "warning" : "success",
|
|
100
|
+
isFailure ? "x" : isUncertain ? "?" : "✓",
|
|
39
101
|
);
|
|
102
|
+
const id = singleLine(result.id);
|
|
103
|
+
const title = result.title ? singleLine(result.title) : "";
|
|
104
|
+
const status = isUncertain
|
|
105
|
+
? "uncertain"
|
|
106
|
+
: singleLine(result.status ?? "settled");
|
|
107
|
+
const elapsed = result.elapsed ? singleLine(result.elapsed) : "";
|
|
108
|
+
const notices = [
|
|
109
|
+
result.artifactSaveFailed ? "artifact not saved" : "",
|
|
110
|
+
result.worktreeBranch
|
|
111
|
+
? `worktree handoff · ${singleLine(result.worktreeBranch)}`
|
|
112
|
+
: "",
|
|
113
|
+
result.fullResultSaved ? "full result saved" : "",
|
|
114
|
+
].filter(Boolean);
|
|
115
|
+
const attention = notices.length
|
|
116
|
+
? theme.fg("warning", `${notices.join(" · ")} · `)
|
|
117
|
+
: "";
|
|
40
118
|
lines.push(
|
|
41
|
-
` ${icon} ${theme.fg("accent",
|
|
119
|
+
` ${icon} ${attention}${theme.fg("accent", id)}${title ? theme.fg("muted", ` · ${title}`) : ""}${theme.fg("dim", ` · ${status}${elapsed ? ` · ${elapsed}` : ""}`)}`,
|
|
42
120
|
);
|
|
43
121
|
}
|
|
44
|
-
if (results.length >
|
|
45
|
-
lines.push(
|
|
122
|
+
if (results.length > statusRows.length) {
|
|
123
|
+
lines.push(
|
|
124
|
+
theme.fg("dim", ` … ${results.length - statusRows.length} more`),
|
|
125
|
+
);
|
|
46
126
|
}
|
|
47
127
|
|
|
48
|
-
if (content.trim()) {
|
|
128
|
+
if (sanitizeText(content).trim()) {
|
|
49
129
|
lines.push(
|
|
50
130
|
theme.fg(
|
|
51
131
|
"dim",
|
|
@@ -56,6 +136,14 @@ export function buildWaitResultPreview(
|
|
|
56
136
|
return lines.join("\n");
|
|
57
137
|
}
|
|
58
138
|
|
|
139
|
+
export function renderWaitResultPreview(
|
|
140
|
+
content: string,
|
|
141
|
+
details: WaitResultDetails | undefined,
|
|
142
|
+
theme: Theme,
|
|
143
|
+
) {
|
|
144
|
+
return fixedRows(buildWaitResultPreview(content, details, theme).split("\n"));
|
|
145
|
+
}
|
|
146
|
+
|
|
59
147
|
export function renderWaitResult(
|
|
60
148
|
content: string,
|
|
61
149
|
details: WaitResultDetails | undefined,
|
|
@@ -63,7 +151,7 @@ export function renderWaitResult(
|
|
|
63
151
|
theme: Theme,
|
|
64
152
|
) {
|
|
65
153
|
if (!expanded) {
|
|
66
|
-
return
|
|
154
|
+
return renderWaitResultPreview(content, details, theme);
|
|
67
155
|
}
|
|
68
156
|
|
|
69
157
|
const markdown = new Markdown(
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
} from "../../shared/below-editor-navigation.ts";
|
|
14
14
|
|
|
15
15
|
const FAKE_CURSOR_PATTERN = /\u001b\[7m \u001b\[(?:0|27)m/;
|
|
16
|
+
// Conservative idle-row band for terminal-owned CJK IME preedit. Pi's editor
|
|
17
|
+
// contract exposes only committed input, never a composition event, so the
|
|
18
|
+
// reservation cannot shrink to the frames that actually need it. A 1-cell
|
|
19
|
+
// hardware-cursor pad lets long preedit cover or truncate the ghost.
|
|
16
20
|
const IME_PREEDIT_MIN_COLUMNS = 12;
|
|
17
21
|
const IME_PREEDIT_MAX_COLUMNS = 32;
|
|
18
22
|
const GHOST_MIN_COLUMNS = 8;
|
|
@@ -96,10 +100,12 @@ function ghostGeometry(lines: readonly string[], width: number) {
|
|
|
96
100
|
.replaceAll(CURSOR_MARKER, "");
|
|
97
101
|
const prefix = `${beforeCursor}${cursor[0]}`;
|
|
98
102
|
const remaining = Math.max(0, width - visibleWidth(prefix));
|
|
99
|
-
//
|
|
100
|
-
// narrower widths, suppress the ghost rather than placing the
|
|
101
|
-
// terminal's out-of-range column width.
|
|
102
|
-
if (remaining
|
|
103
|
+
// Keep enough cells for a readable ghost and a conservative IME preedit
|
|
104
|
+
// band. At narrower widths, suppress the ghost rather than placing the
|
|
105
|
+
// hidden hardware cursor at the terminal's out-of-range column width.
|
|
106
|
+
if (remaining < GHOST_MIN_COLUMNS + IME_PREEDIT_MIN_COLUMNS) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
103
109
|
|
|
104
110
|
const desiredPreedit = Math.min(
|
|
105
111
|
IME_PREEDIT_MAX_COLUMNS,
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
type TaskToolDetails,
|
|
35
35
|
} from "./ui.ts";
|
|
36
36
|
|
|
37
|
-
const TOOL_NAMES = ["tasks_add", "tasks_update", "tasks_list"] as const;
|
|
38
37
|
const TASK_WIDGET_KEY = "session-tasks-panel";
|
|
39
38
|
const CONFLICT_NAMES = new Set(["todo", "TodoWrite", "update_plan"]);
|
|
40
39
|
const TOOL_PURPOSE =
|
|
@@ -542,8 +541,6 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
542
541
|
});
|
|
543
542
|
}
|
|
544
543
|
|
|
545
|
-
export { TOOL_NAMES as TASK_TOOL_NAMES };
|
|
546
|
-
|
|
547
544
|
function mutationWillCloseBatch(
|
|
548
545
|
snapshot: TaskSnapshot,
|
|
549
546
|
id: number,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import {
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
3
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
5
|
getCapabilities,
|
|
@@ -117,10 +117,21 @@ export function formatTokens(tokens: number) {
|
|
|
117
117
|
return `${(tokens / 1_000_000).toFixed(1)}m`;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export function formatDirectory(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
export function formatDirectory(
|
|
121
|
+
cwd: string,
|
|
122
|
+
home = homedir(),
|
|
123
|
+
pathModule = process.platform === "win32" ? win32 : posix,
|
|
124
|
+
) {
|
|
125
|
+
const relativePath = pathModule.relative(home, cwd);
|
|
126
|
+
const outsideHome =
|
|
127
|
+
relativePath === ".." ||
|
|
128
|
+
relativePath.startsWith(`..${pathModule.sep}`) ||
|
|
129
|
+
pathModule.isAbsolute(relativePath);
|
|
130
|
+
const display = outsideHome
|
|
131
|
+
? cwd
|
|
132
|
+
: relativePath
|
|
133
|
+
? `~/${relativePath.replaceAll(pathModule.sep, "/")}`
|
|
134
|
+
: "~";
|
|
124
135
|
return sanitizeTerminalLabel(display);
|
|
125
136
|
}
|
|
126
137
|
|
|
@@ -527,43 +538,3 @@ export function renderFooter(options: FooterRenderOptions) {
|
|
|
527
538
|
|
|
528
539
|
return lines;
|
|
529
540
|
}
|
|
530
|
-
|
|
531
|
-
/** Legacy helper retained for tests that assert grouped content. */
|
|
532
|
-
export function buildFooterContent(
|
|
533
|
-
modelInfo: ModelInfoState,
|
|
534
|
-
gitInfo: GitInfoState,
|
|
535
|
-
selectedItems: readonly FooterItem[],
|
|
536
|
-
formatPullRequest: (number: number, url: string) => string = (number) =>
|
|
537
|
-
`PR #${number}`,
|
|
538
|
-
) {
|
|
539
|
-
const items = new Set(selectedItems);
|
|
540
|
-
const catalog = buildSegmentCatalog(
|
|
541
|
-
"",
|
|
542
|
-
modelInfo,
|
|
543
|
-
gitInfo,
|
|
544
|
-
formatPullRequest,
|
|
545
|
-
);
|
|
546
|
-
return {
|
|
547
|
-
showCwd: items.has("cwd"),
|
|
548
|
-
model: [
|
|
549
|
-
items.has("model") ? catalog.model.text : "",
|
|
550
|
-
items.has("thinking") ? catalog.thinking.text : "",
|
|
551
|
-
]
|
|
552
|
-
.filter(Boolean)
|
|
553
|
-
.join(" · "),
|
|
554
|
-
usage: [
|
|
555
|
-
items.has("context") ? catalog.context.text : "",
|
|
556
|
-
items.has("cache") ? catalog.cache.text : "",
|
|
557
|
-
items.has("cost") ? catalog.cost.text : "",
|
|
558
|
-
items.has("throughput") ? catalog.throughput.text : "",
|
|
559
|
-
]
|
|
560
|
-
.filter(Boolean)
|
|
561
|
-
.join(" · "),
|
|
562
|
-
git: [
|
|
563
|
-
items.has("git") ? catalog.git.text : "",
|
|
564
|
-
items.has("pr") ? catalog.pr.text : "",
|
|
565
|
-
]
|
|
566
|
-
.filter(Boolean)
|
|
567
|
-
.join(" · "),
|
|
568
|
-
};
|
|
569
|
-
}
|
|
@@ -21,13 +21,9 @@ import {
|
|
|
21
21
|
REFRESH_CHANNEL,
|
|
22
22
|
isGitInfoState,
|
|
23
23
|
isModelInfoState,
|
|
24
|
-
type GitInfoState,
|
|
25
|
-
type ModelInfoState,
|
|
26
24
|
} from "../shared/dashboard-state.ts";
|
|
27
25
|
import { formatDirectory, renderFooter } from "./footer.ts";
|
|
28
26
|
|
|
29
|
-
export { buildFooterContent, formatDirectory, renderFooter } from "./footer.ts";
|
|
30
|
-
|
|
31
27
|
type Rgb = [number, number, number];
|
|
32
28
|
|
|
33
29
|
const RESET = "\x1b[0m";
|
|
@@ -40,8 +40,35 @@ type Segment =
|
|
|
40
40
|
| { kind: "prose"; lines: string[] }
|
|
41
41
|
| { kind: "code"; open: string; content: string[]; close: string };
|
|
42
42
|
|
|
43
|
-
const FENCE_OPEN = /^ {0,3}`{3,}/;
|
|
44
|
-
|
|
43
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse an opening code fence (CommonMark §4.5): which character it uses and
|
|
47
|
+
* how long it is. A backtick fence's info string may not contain backticks.
|
|
48
|
+
*/
|
|
49
|
+
function openFence(line: string) {
|
|
50
|
+
const match = FENCE_OPEN.exec(line);
|
|
51
|
+
if (!match) return undefined;
|
|
52
|
+
const fence = match[1];
|
|
53
|
+
if (fence[0] === "`" && line.slice(match[0].length).includes("`")) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { char: fence[0], length: fence.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A closing fence must use the same character as the opening fence and be at
|
|
61
|
+
* least as long: a ``` line does not close a ```` block, and backticks never
|
|
62
|
+
* close a tilde block.
|
|
63
|
+
*/
|
|
64
|
+
function isCloseFence(line: string, open: { char: string; length: number }) {
|
|
65
|
+
const match = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/.exec(line);
|
|
66
|
+
return (
|
|
67
|
+
match !== null &&
|
|
68
|
+
match[1][0] === open.char &&
|
|
69
|
+
match[1].length >= open.length
|
|
70
|
+
);
|
|
71
|
+
}
|
|
45
72
|
|
|
46
73
|
function countLines(markdown: string) {
|
|
47
74
|
const parts = markdown.split("\n");
|
|
@@ -60,7 +87,8 @@ function parseSegments(lines: string[]): Segment[] {
|
|
|
60
87
|
let prose: string[] = [];
|
|
61
88
|
let i = 0;
|
|
62
89
|
while (i < lines.length) {
|
|
63
|
-
|
|
90
|
+
const fence = openFence(lines[i]);
|
|
91
|
+
if (!fence) {
|
|
64
92
|
prose.push(lines[i]);
|
|
65
93
|
i += 1;
|
|
66
94
|
continue;
|
|
@@ -74,13 +102,21 @@ function parseSegments(lines: string[]): Segment[] {
|
|
|
74
102
|
let close: string | undefined;
|
|
75
103
|
let j = i + 1;
|
|
76
104
|
while (j < lines.length && close === undefined) {
|
|
77
|
-
if (
|
|
105
|
+
if (isCloseFence(lines[j], fence)) close = lines[j];
|
|
78
106
|
else content.push(lines[j]);
|
|
79
107
|
j += 1;
|
|
80
108
|
}
|
|
81
109
|
if (close === undefined) {
|
|
82
|
-
// Unterminated fence:
|
|
83
|
-
|
|
110
|
+
// Unterminated fence: the block runs to the end of the message. Keep it
|
|
111
|
+
// as a code block with a synthesized closing fence so a folded preview
|
|
112
|
+
// never leaks an unclosed fence into the TUI.
|
|
113
|
+
segments.push({
|
|
114
|
+
kind: "code",
|
|
115
|
+
open,
|
|
116
|
+
content,
|
|
117
|
+
close: fence.char.repeat(fence.length),
|
|
118
|
+
});
|
|
119
|
+
return segments;
|
|
84
120
|
}
|
|
85
121
|
segments.push({ kind: "code", open, content, close });
|
|
86
122
|
i = j;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionCommandContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
missingPiCodingAgentDiagnostic,
|
|
10
|
+
PI_CODING_AGENT_ENTRY_ENV,
|
|
11
|
+
resolvePiCodingAgentEntry,
|
|
12
|
+
} from "../../web/host/pi-coding-agent-entry.ts";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
export interface WebProcess {
|
|
17
|
+
readonly exitCode: number | null;
|
|
18
|
+
readonly signalCode: NodeJS.Signals | null;
|
|
19
|
+
once(event: "error", listener: (error: Error) => void): this;
|
|
20
|
+
once(
|
|
21
|
+
event: "close",
|
|
22
|
+
listener: (code: number | null, signal: NodeJS.Signals | null) => void,
|
|
23
|
+
): this;
|
|
24
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SpawnWebOptions {
|
|
28
|
+
cwd: string;
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
shell: false;
|
|
31
|
+
stdio: "inherit";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function webProcessEnvironment(
|
|
35
|
+
cwd: string,
|
|
36
|
+
piCodingAgentEntry: string | undefined,
|
|
37
|
+
) {
|
|
38
|
+
const environment: NodeJS.ProcessEnv = { ...process.env, PWD: cwd };
|
|
39
|
+
delete environment.OLDPWD;
|
|
40
|
+
delete environment.INIT_CWD;
|
|
41
|
+
delete environment.PI_SESSION_ID;
|
|
42
|
+
delete environment.PI_SESSION_FILE;
|
|
43
|
+
if (piCodingAgentEntry) {
|
|
44
|
+
environment[PI_CODING_AGENT_ENTRY_ENV] = piCodingAgentEntry;
|
|
45
|
+
} else {
|
|
46
|
+
delete environment[PI_CODING_AGENT_ENTRY_ENV];
|
|
47
|
+
}
|
|
48
|
+
return environment;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WebCommandDependencies {
|
|
52
|
+
entrypoint: string;
|
|
53
|
+
spawn(command: string, args: string[], options: SpawnWebOptions): WebProcess;
|
|
54
|
+
clearTerminal(): void;
|
|
55
|
+
holdParentSigint(): () => void;
|
|
56
|
+
resolvePiCodingAgentEntry(): string | undefined;
|
|
57
|
+
shutdownTimeoutMs: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type WebExit =
|
|
61
|
+
| { kind: "close"; code: number | null; signal: NodeJS.Signals | null }
|
|
62
|
+
| { kind: "error"; error: Error };
|
|
63
|
+
|
|
64
|
+
interface ActiveWebProcess {
|
|
65
|
+
child: WebProcess;
|
|
66
|
+
closed: Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defaultDependencies: WebCommandDependencies = {
|
|
70
|
+
entrypoint: fileURLToPath(new URL("../../bin/openpi.js", import.meta.url)),
|
|
71
|
+
spawn(command, args, options) {
|
|
72
|
+
return nodeSpawn(command, args, options);
|
|
73
|
+
},
|
|
74
|
+
clearTerminal() {
|
|
75
|
+
process.stdout.write("\u001b[2J\u001b[H");
|
|
76
|
+
},
|
|
77
|
+
holdParentSigint() {
|
|
78
|
+
const keepPiAlive = () => {};
|
|
79
|
+
process.on("SIGINT", keepPiAlive);
|
|
80
|
+
return () => process.removeListener("SIGINT", keepPiAlive);
|
|
81
|
+
},
|
|
82
|
+
resolvePiCodingAgentEntry: () =>
|
|
83
|
+
resolvePiCodingAgentEntry({ source: "host" }),
|
|
84
|
+
shutdownTimeoutMs: DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function delay(milliseconds: number) {
|
|
88
|
+
return new Promise<void>((resolve) => {
|
|
89
|
+
const timer = setTimeout(resolve, milliseconds);
|
|
90
|
+
timer.unref();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function stopWebProcess(active: ActiveWebProcess, timeoutMs: number) {
|
|
95
|
+
if (active.child.exitCode !== null || active.child.signalCode !== null)
|
|
96
|
+
return;
|
|
97
|
+
active.child.kill("SIGTERM");
|
|
98
|
+
const timedOut = await Promise.race([
|
|
99
|
+
active.closed.then(() => false),
|
|
100
|
+
delay(timeoutMs).then(() => true),
|
|
101
|
+
]);
|
|
102
|
+
if (!timedOut) return;
|
|
103
|
+
if (active.child.exitCode === null && active.child.signalCode === null) {
|
|
104
|
+
active.child.kill("SIGKILL");
|
|
105
|
+
await Promise.race([active.closed, delay(timeoutMs)]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function runWebInForeground(
|
|
110
|
+
ctx: ExtensionCommandContext,
|
|
111
|
+
dependencies: WebCommandDependencies,
|
|
112
|
+
setActive: (active: ActiveWebProcess | undefined) => void,
|
|
113
|
+
isShuttingDown: () => boolean,
|
|
114
|
+
piCodingAgentEntry: string,
|
|
115
|
+
) {
|
|
116
|
+
return ctx.ui.custom<WebExit>((tui, _theme, _keybindings, done) => {
|
|
117
|
+
let finished = false;
|
|
118
|
+
let tuiStopped = false;
|
|
119
|
+
let resolveClosed = () => {};
|
|
120
|
+
const closed = new Promise<void>((resolve) => {
|
|
121
|
+
resolveClosed = resolve;
|
|
122
|
+
});
|
|
123
|
+
const releaseParentSigint = dependencies.holdParentSigint();
|
|
124
|
+
|
|
125
|
+
const finish = (result: WebExit) => {
|
|
126
|
+
if (finished) return;
|
|
127
|
+
finished = true;
|
|
128
|
+
releaseParentSigint();
|
|
129
|
+
setActive(undefined);
|
|
130
|
+
resolveClosed();
|
|
131
|
+
if (tuiStopped && !isShuttingDown()) {
|
|
132
|
+
tui.start();
|
|
133
|
+
tui.requestRender(true);
|
|
134
|
+
}
|
|
135
|
+
done(result);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
tui.stop();
|
|
140
|
+
tuiStopped = true;
|
|
141
|
+
dependencies.clearTerminal();
|
|
142
|
+
const childCwd = dirname(dependencies.entrypoint);
|
|
143
|
+
const child = dependencies.spawn(
|
|
144
|
+
process.execPath,
|
|
145
|
+
[dependencies.entrypoint, "web", "--no-workspace"],
|
|
146
|
+
{
|
|
147
|
+
cwd: childCwd,
|
|
148
|
+
env: webProcessEnvironment(childCwd, piCodingAgentEntry),
|
|
149
|
+
shell: false,
|
|
150
|
+
stdio: "inherit",
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
setActive({ child, closed });
|
|
154
|
+
child.once("error", (error) => finish({ kind: "error", error }));
|
|
155
|
+
child.once("close", (code, signal) =>
|
|
156
|
+
finish({ kind: "close", code, signal }),
|
|
157
|
+
);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
finish({
|
|
160
|
+
kind: "error",
|
|
161
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { render: () => [], invalidate: () => {} };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default function web(
|
|
170
|
+
pi: ExtensionAPI,
|
|
171
|
+
dependencies: WebCommandDependencies = defaultDependencies,
|
|
172
|
+
) {
|
|
173
|
+
let active: ActiveWebProcess | undefined;
|
|
174
|
+
let running = false;
|
|
175
|
+
let shuttingDown = false;
|
|
176
|
+
|
|
177
|
+
pi.on("session_start", () => {
|
|
178
|
+
shuttingDown = false;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
pi.on("session_shutdown", async () => {
|
|
182
|
+
shuttingDown = true;
|
|
183
|
+
if (active) {
|
|
184
|
+
await stopWebProcess(active, dependencies.shutdownTimeoutMs);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.registerCommand("web", {
|
|
189
|
+
description:
|
|
190
|
+
"Open the separate OpenPI Web Workbench in this terminal until Ctrl+C",
|
|
191
|
+
handler: async (args, ctx) => {
|
|
192
|
+
if (args.trim()) {
|
|
193
|
+
ctx.ui.notify("Usage: /web", "warning");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (ctx.mode !== "tui") {
|
|
197
|
+
ctx.ui.notify("/web requires the interactive TUI.", "warning");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
201
|
+
ctx.ui.notify(
|
|
202
|
+
"Wait until the current Pi Session is idle before starting /web.",
|
|
203
|
+
"warning",
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (running) {
|
|
208
|
+
ctx.ui.notify("OpenPI Web Workbench is already running.", "warning");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const piCodingAgentEntry = dependencies.resolvePiCodingAgentEntry();
|
|
212
|
+
if (!piCodingAgentEntry) {
|
|
213
|
+
ctx.ui.notify(missingPiCodingAgentDiagnostic(), "error");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
running = true;
|
|
218
|
+
try {
|
|
219
|
+
const result = await runWebInForeground(
|
|
220
|
+
ctx,
|
|
221
|
+
dependencies,
|
|
222
|
+
(next) => {
|
|
223
|
+
active = next;
|
|
224
|
+
},
|
|
225
|
+
() => shuttingDown,
|
|
226
|
+
piCodingAgentEntry,
|
|
227
|
+
);
|
|
228
|
+
if (shuttingDown) return;
|
|
229
|
+
if (result.kind === "error") {
|
|
230
|
+
ctx.ui.notify(
|
|
231
|
+
`Failed to start OpenPI Web Workbench: ${result.error.message}`,
|
|
232
|
+
"error",
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (result.signal !== null) {
|
|
237
|
+
ctx.ui.notify(
|
|
238
|
+
`OpenPI Web Workbench was terminated by ${result.signal}.`,
|
|
239
|
+
"error",
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (result.code !== 0) {
|
|
244
|
+
ctx.ui.notify(
|
|
245
|
+
`OpenPI Web Workbench exited with code ${result.code ?? "unknown"}.`,
|
|
246
|
+
"error",
|
|
247
|
+
);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
ctx.ui.notify("OpenPI Web Workbench stopped.", "info");
|
|
251
|
+
} finally {
|
|
252
|
+
active = undefined;
|
|
253
|
+
running = false;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|