@oh-my-pi/pi-coding-agent 15.11.8 → 15.12.1
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/CHANGELOG.md +46 -2
- package/dist/cli.js +8095 -7704
- package/dist/types/collab/crypto.d.ts +1 -6
- package/dist/types/collab/guest.d.ts +2 -0
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +14 -1
- package/dist/types/config/settings-schema.d.ts +52 -6
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -1
- package/dist/types/export/share.d.ts +43 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +19 -1
- package/dist/types/modes/components/status-line/component.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/session-observer-registry.d.ts +7 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/package.json +13 -14
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/collab/crypto.ts +10 -4
- package/src/collab/guest.ts +31 -2
- package/src/collab/host.ts +73 -11
- package/src/collab/protocol.ts +48 -7
- package/src/commands/join.ts +1 -1
- package/src/config/settings-schema.ts +54 -5
- package/src/config/settings.ts +12 -0
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +122 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +240 -915
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +268 -0
- package/src/internal-urls/docs-index.generated.ts +73 -73
- package/src/lsp/index.ts +11 -0
- package/src/main.ts +22 -9
- package/src/modes/components/agent-hub.ts +541 -410
- package/src/modes/components/status-line/component.ts +38 -5
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tips.txt +3 -1
- package/src/modes/controllers/command-controller.ts +55 -96
- package/src/modes/controllers/event-controller.ts +45 -16
- package/src/modes/controllers/input-controller.ts +104 -4
- package/src/modes/controllers/selector-controller.ts +11 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/interactive-mode.ts +44 -2
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +16 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/session/agent-session.ts +87 -19
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +62 -35
- package/src/task/executor.ts +14 -0
- package/src/task/index.ts +5 -1
- package/src/task/render.ts +76 -5
- package/src/task/types.ts +9 -0
- package/src/tiny/worker.ts +17 -95
- package/src/tools/ast-grep.ts +3 -1
- package/src/tools/find.ts +3 -1
- package/src/tools/gh.ts +20 -6
- package/src/tools/irc.ts +4 -0
- package/src/tools/job.ts +18 -13
- package/src/tools/memory-recall.ts +2 -0
- package/src/tools/search.ts +3 -1
- package/src/tools/tool-result.ts +8 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/tiny/compiled-runtime.d.ts +0 -35
- package/scripts/generate-template.ts +0 -33
- package/src/bun-imports.d.ts +0 -28
- package/src/export/html/template.generated.ts +0 -2
- package/src/export/html/template.macro.ts +0 -25
- package/src/tiny/compiled-runtime.ts +0 -179
package/src/export/html/index.ts
CHANGED
|
@@ -1,14 +1,47 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import type { AgentState } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import { APP_NAME, isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
loadEntriesFromFile,
|
|
8
|
+
type SessionEntry,
|
|
9
|
+
type SessionHeader,
|
|
10
|
+
SessionManager,
|
|
11
|
+
} from "../../session/session-manager";
|
|
12
|
+
import templateCss from "./template.css" with { type: "text" };
|
|
13
|
+
import templateHtml from "./template.html" with { type: "text" };
|
|
14
|
+
import templateJs from "./template.js" with { type: "text" };
|
|
15
|
+
// Pre-built React tool renderers: built by `bun --cwd=packages/collab-web run build:tool-views`,
|
|
16
|
+
// run automatically by root `prepare` on install and by `prepack` at publish.
|
|
17
|
+
import toolViewsJs from "./tool-views.generated.js" with { type: "text" };
|
|
18
|
+
|
|
19
|
+
let cachedTemplate: string | undefined;
|
|
20
|
+
|
|
21
|
+
/** Compose the standalone export template: minified CSS, tool renderers, and viewer JS inlined. */
|
|
22
|
+
export function getTemplate(): string {
|
|
23
|
+
if (cachedTemplate) return cachedTemplate;
|
|
24
|
+
const minifiedCss = templateCss
|
|
25
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
26
|
+
.replace(/\s+/g, " ")
|
|
27
|
+
.replace(/\s*([{}:;,])\s*/g, "$1")
|
|
28
|
+
.trim();
|
|
29
|
+
// Function replacements so `$'`, `$&`, `$$`, etc. inside the embedded
|
|
30
|
+
// CSS/JS are not interpreted as substitution patterns. The cast is safe:
|
|
31
|
+
// `with { type: "text" }` yields a string at runtime; bun-types just types
|
|
32
|
+
// every *.html import as HTMLBundle (TS can't vary types by import attribute).
|
|
33
|
+
cachedTemplate = (templateHtml as unknown as string)
|
|
34
|
+
.replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
|
|
35
|
+
.replace("<template-tool-views/>", () => `<script>${toolViewsJs}</script>`)
|
|
36
|
+
.replace("<template-js/>", () => `<script>${templateJs}</script>`);
|
|
37
|
+
return cachedTemplate;
|
|
38
|
+
}
|
|
8
39
|
|
|
9
40
|
export interface ExportOptions {
|
|
10
41
|
outputPath?: string;
|
|
11
42
|
themeName?: string;
|
|
43
|
+
/** Embed subagent session transcripts found next to the session file (default true). */
|
|
44
|
+
includeSubSessions?: boolean;
|
|
12
45
|
}
|
|
13
46
|
|
|
14
47
|
/** Parse a color string to RGB values. */
|
|
@@ -71,8 +104,8 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string
|
|
|
71
104
|
};
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
/** Generate CSS custom properties for theme. */
|
|
75
|
-
async function generateThemeVars(themeName?: string): Promise<string> {
|
|
107
|
+
/** Generate CSS custom properties for theme. Exported for the share-viewer build script. */
|
|
108
|
+
export async function generateThemeVars(themeName?: string): Promise<string> {
|
|
76
109
|
const colors = await getResolvedThemeColors(themeName);
|
|
77
110
|
const lines: string[] = [];
|
|
78
111
|
for (const [key, value] of Object.entries(colors)) {
|
|
@@ -90,12 +123,83 @@ async function generateThemeVars(themeName?: string): Promise<string> {
|
|
|
90
123
|
return lines.join(" ");
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
|
|
126
|
+
/** Embedded subagent session transcript, keyed by slash-joined agent path in `SessionData.subSessions`. */
|
|
127
|
+
export interface SubSession {
|
|
128
|
+
/** Bare agent id (session file stem), e.g. "ToolAsk". */
|
|
129
|
+
agentId: string;
|
|
130
|
+
/** Key of the parent sub-session, or null when spawned by the main session. */
|
|
131
|
+
parent: string | null;
|
|
132
|
+
header: SessionHeader | null;
|
|
133
|
+
entries: SessionEntry[];
|
|
134
|
+
leafId: string | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface SessionData {
|
|
94
138
|
header: SessionHeader | null;
|
|
95
139
|
entries: SessionEntry[];
|
|
96
140
|
leafId: string | null;
|
|
97
141
|
systemPrompt?: string;
|
|
98
142
|
tools?: { name: string; description: string }[];
|
|
143
|
+
subSessions?: Record<string, SubSession>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Snapshot the session (plus optional agent state) into the JSON shape the viewer renders. */
|
|
147
|
+
export function buildSessionData(sm: SessionManager, state?: AgentState): SessionData {
|
|
148
|
+
return {
|
|
149
|
+
header: sm.getHeader(),
|
|
150
|
+
entries: sm.getEntries(),
|
|
151
|
+
leafId: sm.getLeafId(),
|
|
152
|
+
systemPrompt: state?.systemPrompt.join("\n\n"),
|
|
153
|
+
tools: state?.tools?.map(t => ({ name: t.name, description: t.description })),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Collect subagent session transcripts stored next to a session file.
|
|
159
|
+
*
|
|
160
|
+
* A session at `<dir>/<name>.jsonl` keeps its subagent sessions at `<dir>/<name>/<AgentId>.jsonl`;
|
|
161
|
+
* each subagent's own children nest the same way under `<dir>/<name>/<AgentId>/`. Keys in the
|
|
162
|
+
* returned record are slash-joined ids relative to the main session ("ToolAsk", "ToolAsk/Helper").
|
|
163
|
+
* Corrupt or empty files are skipped silently.
|
|
164
|
+
*/
|
|
165
|
+
export async function collectSubSessions(sessionFile: string): Promise<Record<string, SubSession>> {
|
|
166
|
+
const result: Record<string, SubSession> = {};
|
|
167
|
+
if (!sessionFile.endsWith(".jsonl")) return result;
|
|
168
|
+
await collectSubSessionsFromDir(sessionFile.slice(0, -6), null, result);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function collectSubSessionsFromDir(
|
|
173
|
+
dir: string,
|
|
174
|
+
parentKey: string | null,
|
|
175
|
+
out: Record<string, SubSession>,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
let names: string[];
|
|
178
|
+
try {
|
|
179
|
+
names = await fs.readdir(dir);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (isEnoent(err)) return;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
if (!name.endsWith(".jsonl") || name.includes(".bak")) continue;
|
|
186
|
+
const agentId = name.slice(0, -6);
|
|
187
|
+
const key = parentKey ? `${parentKey}/${agentId}` : agentId;
|
|
188
|
+
const fileEntries = await loadEntriesFromFile(path.join(dir, name));
|
|
189
|
+
// Empty/corrupt files (no valid session header) load as [] — skip silently.
|
|
190
|
+
if (fileEntries.length > 0) {
|
|
191
|
+
const header = (fileEntries.find(e => e.type === "session") as SessionHeader | undefined) ?? null;
|
|
192
|
+
const entries = fileEntries.filter((e): e is SessionEntry => e.type !== "session");
|
|
193
|
+
out[key] = {
|
|
194
|
+
agentId,
|
|
195
|
+
parent: parentKey,
|
|
196
|
+
header,
|
|
197
|
+
entries,
|
|
198
|
+
leafId: entries.length > 0 ? entries[entries.length - 1].id : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
await collectSubSessionsFromDir(path.join(dir, agentId), key, out);
|
|
202
|
+
}
|
|
99
203
|
}
|
|
100
204
|
|
|
101
205
|
/** Generate HTML from bundled template with runtime substitutions. */
|
|
@@ -106,10 +210,9 @@ async function generateHtml(sessionData: SessionData, themeName?: string): Promi
|
|
|
106
210
|
// Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the
|
|
107
211
|
// substituted CSS/base64 are not interpreted as substitution patterns
|
|
108
212
|
// (see https://mdn.io/String.replace).
|
|
109
|
-
return
|
|
110
|
-
"{{
|
|
111
|
-
() => sessionDataBase64
|
|
112
|
-
);
|
|
213
|
+
return getTemplate()
|
|
214
|
+
.replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`)
|
|
215
|
+
.replace("{{SESSION_DATA}}", () => sessionDataBase64);
|
|
113
216
|
}
|
|
114
217
|
|
|
115
218
|
/** Export session to HTML using SessionManager and AgentState. */
|
|
@@ -123,13 +226,11 @@ export async function exportSessionToHtml(
|
|
|
123
226
|
const sessionFile = sm.getSessionFile();
|
|
124
227
|
if (!sessionFile) throw new Error("Cannot export in-memory session to HTML");
|
|
125
228
|
|
|
126
|
-
const sessionData
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
tools: state?.tools?.map(t => ({ name: t.name, description: t.description })),
|
|
132
|
-
};
|
|
229
|
+
const sessionData = buildSessionData(sm, state);
|
|
230
|
+
if (opts.includeSubSessions !== false) {
|
|
231
|
+
const subSessions = await collectSubSessions(sessionFile);
|
|
232
|
+
if (Object.keys(subSessions).length > 0) sessionData.subSessions = subSessions;
|
|
233
|
+
}
|
|
133
234
|
|
|
134
235
|
const html = await generateHtml(sessionData, opts.themeName);
|
|
135
236
|
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`;
|
|
@@ -155,6 +256,10 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
|
|
|
155
256
|
entries: sm.getEntries(),
|
|
156
257
|
leafId: sm.getLeafId(),
|
|
157
258
|
};
|
|
259
|
+
if (opts.includeSubSessions !== false) {
|
|
260
|
+
const subSessions = await collectSubSessions(inputPath);
|
|
261
|
+
if (Object.keys(subSessions).length > 0) sessionData.subSessions = subSessions;
|
|
262
|
+
}
|
|
158
263
|
|
|
159
264
|
const html = await generateHtml(sessionData, opts.themeName);
|
|
160
265
|
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// SHARE VIEWER BOOTSTRAP
|
|
6
|
+
// ============================================================
|
|
7
|
+
//
|
|
8
|
+
// Served by the omp relay at /s/<id>; the AES-256-GCM key rides in the
|
|
9
|
+
// URL fragment and never leaves the browser. Resolves the session JSON
|
|
10
|
+
// and hands it to template.js via `window.__OMP_SESSION_DATA__`:
|
|
11
|
+
// 1. hex ids -> secret GitHub gist holding base64(sealed blob)
|
|
12
|
+
// 2. anything else -> relay blob store at /s/<id>/raw
|
|
13
|
+
// Sealed layout: [12B IV][AES-256-GCM(gzip(session JSON))].
|
|
14
|
+
|
|
15
|
+
var GIST_ID_RE = /^[0-9a-f]{20,64}$/;
|
|
16
|
+
var SHARE_PATH_RE = /\/s\/([A-Za-z0-9_-]{10,64})\/?$/;
|
|
17
|
+
|
|
18
|
+
function decodeBase64(text) {
|
|
19
|
+
var binary = atob(text);
|
|
20
|
+
var bytes = new Uint8Array(binary.length);
|
|
21
|
+
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
22
|
+
return bytes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodeBase64Url(text) {
|
|
26
|
+
var b64 = text.replace(/-/g, '+').replace(/_/g, '/');
|
|
27
|
+
while (b64.length % 4) b64 += '=';
|
|
28
|
+
return decodeBase64(b64);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchGistBlob(id) {
|
|
32
|
+
var res = await fetch('https://api.github.com/gists/' + id, {
|
|
33
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
34
|
+
});
|
|
35
|
+
if (res.status === 404) throw new Error('This share no longer exists (gist deleted?).');
|
|
36
|
+
if (!res.ok) throw new Error('Gist fetch failed: HTTP ' + res.status);
|
|
37
|
+
var gist = await res.json();
|
|
38
|
+
var files = Object.values(gist.files || {});
|
|
39
|
+
var file = files.find(function(f) { return /\.ompshare\.txt$/.test(f.filename); }) || files[0];
|
|
40
|
+
if (!file) throw new Error('Gist has no files.');
|
|
41
|
+
var text = file.content;
|
|
42
|
+
if (!text || file.truncated) {
|
|
43
|
+
var raw = await fetch(file.raw_url);
|
|
44
|
+
if (!raw.ok) throw new Error('Gist raw fetch failed: HTTP ' + raw.status);
|
|
45
|
+
text = await raw.text();
|
|
46
|
+
}
|
|
47
|
+
return decodeBase64(text.replace(/\s+/g, ''));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchServerBlob(id) {
|
|
51
|
+
var res = await fetch('/s/' + id + '/raw');
|
|
52
|
+
if (res.status === 404 || res.status === 410) {
|
|
53
|
+
throw new Error('This share no longer exists (expired or deleted).');
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) throw new Error('Share fetch failed: HTTP ' + res.status);
|
|
56
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function load() {
|
|
60
|
+
var match = SHARE_PATH_RE.exec(location.pathname);
|
|
61
|
+
if (!match) throw new Error('Bad share URL; expected /s/<id>.');
|
|
62
|
+
var keyText = location.hash.replace(/^#/, '');
|
|
63
|
+
if (!keyText) throw new Error('Share link is missing its #key fragment; paste the full link.');
|
|
64
|
+
var keyBytes;
|
|
65
|
+
try {
|
|
66
|
+
keyBytes = decodeBase64Url(keyText);
|
|
67
|
+
} catch (_err) {
|
|
68
|
+
throw new Error('Share key is not valid base64url.');
|
|
69
|
+
}
|
|
70
|
+
if (keyBytes.length !== 32) throw new Error('Share key must decode to 32 bytes.');
|
|
71
|
+
|
|
72
|
+
var id = match[1];
|
|
73
|
+
var sealed = await (GIST_ID_RE.test(id) ? fetchGistBlob(id) : fetchServerBlob(id));
|
|
74
|
+
if (sealed.length <= 12) throw new Error('Sealed session blob is truncated.');
|
|
75
|
+
|
|
76
|
+
var key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt']);
|
|
77
|
+
var plain;
|
|
78
|
+
try {
|
|
79
|
+
plain = await crypto.subtle.decrypt(
|
|
80
|
+
{ name: 'AES-GCM', iv: sealed.subarray(0, 12) },
|
|
81
|
+
key,
|
|
82
|
+
sealed.subarray(12)
|
|
83
|
+
);
|
|
84
|
+
} catch (_err) {
|
|
85
|
+
throw new Error('Decryption failed: wrong or corrupted #key.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var data = await new Response(
|
|
89
|
+
new Blob([plain]).stream().pipeThrough(new DecompressionStream('gzip'))
|
|
90
|
+
).json();
|
|
91
|
+
if (data && data.header && data.header.title) {
|
|
92
|
+
document.title = data.header.title + ' — omp session';
|
|
93
|
+
}
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var pending = load();
|
|
98
|
+
// template.js surfaces the failure in-page; swallow the duplicate here
|
|
99
|
+
// so the console does not report an unhandled rejection.
|
|
100
|
+
pending.catch(function() {});
|
|
101
|
+
window.__OMP_SESSION_DATA__ = pending;
|
|
102
|
+
})();
|