@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.5
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 +89 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- package/src/eval/parse.ts +156 -255
- package/src/eval/py/executor.ts +24 -8
- package/src/eval/py/index.ts +1 -0
- package/src/eval/py/prelude.py +11 -80
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +50 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +229 -17
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/grammar.lark +16 -23
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -1
- package/src/internal-urls/agent-protocol.ts +64 -52
- package/src/internal-urls/artifact-protocol.ts +52 -51
- package/src/internal-urls/docs-index.generated.ts +34 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +50 -7
- package/src/internal-urls/mcp-protocol.ts +3 -8
- package/src/internal-urls/memory-protocol.ts +90 -59
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +40 -23
- package/src/internal-urls/rule-protocol.ts +3 -20
- package/src/internal-urls/skill-protocol.ts +5 -27
- package/src/internal-urls/types.ts +18 -2
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/project-prompt.md +10 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +159 -232
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -34
- package/src/prompts/tools/eval.md +27 -16
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +107 -37
- package/src/session/artifacts.ts +7 -4
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +3 -9
- package/src/task/executor.ts +23 -7
- package/src/task/index.ts +57 -36
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +30 -50
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +59 -44
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +74 -31
- package/src/tools/search.ts +16 -3
- package/src/tools/todo-write.ts +1 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -119
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
|
@@ -456,6 +456,10 @@
|
|
|
456
456
|
const content = truncate(normalize(extractContent(msg.content)));
|
|
457
457
|
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
|
|
458
458
|
}
|
|
459
|
+
if (msg.role === 'developer') {
|
|
460
|
+
const content = truncate(normalize(extractContent(msg.content)));
|
|
461
|
+
return labelHtml + `<span class="tree-role-developer">developer:</span> ${escapeHtml(content)}`;
|
|
462
|
+
}
|
|
459
463
|
if (msg.role === 'assistant') {
|
|
460
464
|
const textContent = truncate(normalize(extractContent(msg.content)));
|
|
461
465
|
if (textContent) {
|
|
@@ -839,9 +843,11 @@
|
|
|
839
843
|
|
|
840
844
|
function renderEdit(name, args, result, ctx) {
|
|
841
845
|
const filePath = str(args.file_path == null ? args.path : args.file_path);
|
|
842
|
-
const pathHtml = filePath
|
|
846
|
+
const pathHtml = filePath ? escapeHtml(shortenPath(filePath)) : '';
|
|
843
847
|
let html = toolHead('edit', pathHtml);
|
|
844
|
-
if (
|
|
848
|
+
if (typeof args.input === 'string' && args.input.length) {
|
|
849
|
+
html += codeBlock(args.input, null);
|
|
850
|
+
} else if (Array.isArray(args.edits)) {
|
|
845
851
|
html += '<div class="tool-args">';
|
|
846
852
|
for (const e of args.edits) {
|
|
847
853
|
const op = e && typeof e.op === 'string' ? e.op : '?';
|
|
@@ -867,7 +873,8 @@
|
|
|
867
873
|
|
|
868
874
|
function renderAstEdit(name, args, result, ctx) {
|
|
869
875
|
const lang = args.lang || null;
|
|
870
|
-
const
|
|
876
|
+
const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (args.path ? shortenPath(String(args.path)) : '');
|
|
877
|
+
const pathHtml = paths ? escapeHtml(paths) : '';
|
|
871
878
|
const badges = [];
|
|
872
879
|
if (lang) badges.push(lang);
|
|
873
880
|
if (args.glob) badges.push('glob=' + args.glob);
|
|
@@ -931,10 +938,12 @@
|
|
|
931
938
|
}
|
|
932
939
|
|
|
933
940
|
function renderFind(name, args, result, ctx) {
|
|
934
|
-
const
|
|
935
|
-
const patHtml =
|
|
936
|
-
const badges =
|
|
937
|
-
|
|
941
|
+
const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (str(args.pattern) || '.');
|
|
942
|
+
const patHtml = paths ? escapeHtml(paths) : invalidArgHtml();
|
|
943
|
+
const badges = [];
|
|
944
|
+
if (args.limit) badges.push('limit=' + args.limit);
|
|
945
|
+
if (args.hidden === false) badges.push('no-hidden');
|
|
946
|
+
let html = toolHead('find', '<span class="tool-pattern">' + patHtml + '</span>', badges.length ? badges : null);
|
|
938
947
|
if (result) {
|
|
939
948
|
const output = ctx.getResultText();
|
|
940
949
|
if (output) html += formatExpandableOutput(output, 10);
|
|
@@ -1168,14 +1177,18 @@
|
|
|
1168
1177
|
}
|
|
1169
1178
|
|
|
1170
1179
|
function renderGh(name, args, result, ctx) {
|
|
1180
|
+
const op = str(args.op);
|
|
1171
1181
|
const badges = [];
|
|
1182
|
+
if (op) badges.push(op);
|
|
1172
1183
|
if (args.repo) badges.push(String(args.repo));
|
|
1173
1184
|
if (args.issue) badges.push('#' + args.issue);
|
|
1174
|
-
if (args.pr) badges.push('PR ' + args.pr);
|
|
1185
|
+
if (args.pr) badges.push(Array.isArray(args.pr) ? 'PRs ' + args.pr.join(',') : 'PR ' + args.pr);
|
|
1175
1186
|
if (args.branch) badges.push('branch=' + args.branch);
|
|
1176
|
-
if (args.query) badges.push('query=' + args.query);
|
|
1187
|
+
if (args.query) badges.push('query=' + truncate(String(args.query), 60));
|
|
1177
1188
|
if (args.run) badges.push('run=' + args.run);
|
|
1189
|
+
if (args.title) badges.push('title=' + truncate(String(args.title), 40));
|
|
1178
1190
|
let html = toolHead(name, '', badges);
|
|
1191
|
+
if (args.body) html += '<div class="tool-output"><div>' + escapeHtml(truncate(String(args.body), 400)) + '</div></div>';
|
|
1179
1192
|
if (result) {
|
|
1180
1193
|
const output = ctx.getResultText();
|
|
1181
1194
|
if (output) html += formatExpandableOutput(output, 12, 'markdown');
|
|
@@ -1249,6 +1262,178 @@
|
|
|
1249
1262
|
return html;
|
|
1250
1263
|
}
|
|
1251
1264
|
|
|
1265
|
+
// Parse `*** Begin <LANG>` cell headers (canonical) and the legacy
|
|
1266
|
+
// `===== <info> =====` headers used by older transcripts. Cells emitted
|
|
1267
|
+
// before the format cutover still need to render in HTML exports.
|
|
1268
|
+
function parseEvalCells(input) {
|
|
1269
|
+
const text = String(input);
|
|
1270
|
+
if (/^[*]{2,}\s*Begin\b/im.test(text)) return parseEvalCellsNew(text);
|
|
1271
|
+
return parseEvalCellsLegacy(text);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function evalLangAlias(token) {
|
|
1275
|
+
const t = String(token || '').toUpperCase();
|
|
1276
|
+
if (t === 'PY' || t === 'PYTHON' || t === 'IPY' || t === 'IPYTHON') return 'py';
|
|
1277
|
+
if (t === 'JS' || t === 'JAVASCRIPT') return 'js';
|
|
1278
|
+
if (t === 'TS' || t === 'TYPESCRIPT') return 'ts';
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function parseEvalCellsNew(text) {
|
|
1283
|
+
const STARS = '\\*{2,}';
|
|
1284
|
+
const BEGIN = new RegExp('^' + STARS + '\\s*Begin\\b\\s*(\\S+)?\\s*$', 'i');
|
|
1285
|
+
const END = new RegExp('^' + STARS + '\\s*End\\b.*$', 'i');
|
|
1286
|
+
const TITLE = new RegExp('^' + STARS + '\\s*Title\\s*:\\s*(.+?)\\s*$', 'i');
|
|
1287
|
+
const TIMEOUT = new RegExp('^' + STARS + '\\s*Timeout\\s*:\\s*(\\S+)\\s*$', 'i');
|
|
1288
|
+
const RESET = new RegExp('^' + STARS + '\\s*Reset\\s*$', 'i');
|
|
1289
|
+
const lines = text.split('\n');
|
|
1290
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
1291
|
+
const cells = [];
|
|
1292
|
+
let i = 0;
|
|
1293
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
1294
|
+
while (i < lines.length) {
|
|
1295
|
+
const beginMatch = BEGIN.exec(lines[i]);
|
|
1296
|
+
if (!beginMatch) { i++; continue; }
|
|
1297
|
+
const lang = evalLangAlias(beginMatch[1]) || 'py';
|
|
1298
|
+
i++;
|
|
1299
|
+
let title = '';
|
|
1300
|
+
const attrs = [];
|
|
1301
|
+
while (i < lines.length) {
|
|
1302
|
+
const tm = TITLE.exec(lines[i]);
|
|
1303
|
+
if (tm) { if (!title) title = tm[1]; i++; continue; }
|
|
1304
|
+
const to = TIMEOUT.exec(lines[i]);
|
|
1305
|
+
if (to) { attrs.push('t=' + to[1]); i++; continue; }
|
|
1306
|
+
if (RESET.test(lines[i])) { attrs.push('rst'); i++; continue; }
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
const codeLines = [];
|
|
1310
|
+
while (i < lines.length) {
|
|
1311
|
+
if (END.test(lines[i])) { i++; break; }
|
|
1312
|
+
if (BEGIN.test(lines[i])) break;
|
|
1313
|
+
codeLines.push(lines[i]);
|
|
1314
|
+
i++;
|
|
1315
|
+
}
|
|
1316
|
+
while (codeLines.length && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
|
|
1317
|
+
cells.push({ lang, title, attrs, code: codeLines.join('\n') });
|
|
1318
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
1319
|
+
}
|
|
1320
|
+
return cells;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function parseEvalCellsLegacy(input) {
|
|
1324
|
+
const HEADER = /^={5,}\s*(.*?)\s*={5,}\s*$/;
|
|
1325
|
+
const lines = String(input).split('\n');
|
|
1326
|
+
const cells = [];
|
|
1327
|
+
let inheritedLang = 'py';
|
|
1328
|
+
let current = null;
|
|
1329
|
+
for (const line of lines) {
|
|
1330
|
+
const m = line.match(HEADER);
|
|
1331
|
+
if (m) {
|
|
1332
|
+
if (current) cells.push(current);
|
|
1333
|
+
const info = m[1] || '';
|
|
1334
|
+
let lang = inheritedLang;
|
|
1335
|
+
let title = '';
|
|
1336
|
+
const langMatch = info.match(/^(py|js|ts)(?::"([^"]*)")?/);
|
|
1337
|
+
if (langMatch) {
|
|
1338
|
+
lang = langMatch[1];
|
|
1339
|
+
if (langMatch[2]) title = langMatch[2];
|
|
1340
|
+
}
|
|
1341
|
+
if (!title) {
|
|
1342
|
+
const idMatch = info.match(/id:"([^"]*)"/);
|
|
1343
|
+
if (idMatch) title = idMatch[1];
|
|
1344
|
+
}
|
|
1345
|
+
inheritedLang = lang;
|
|
1346
|
+
const attrs = [];
|
|
1347
|
+
const tMatch = info.match(/(?:^|\s)t:(\S+)/);
|
|
1348
|
+
if (tMatch) attrs.push('t=' + tMatch[1]);
|
|
1349
|
+
if (/(?:^|\s)rst(?:\s|$)/.test(info)) attrs.push('rst');
|
|
1350
|
+
current = { lang, title, attrs, code: '' };
|
|
1351
|
+
} else {
|
|
1352
|
+
if (!current) current = { lang: inheritedLang, title: '', attrs: [], code: '' };
|
|
1353
|
+
current.code += (current.code ? '\n' : '') + line;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (current) cells.push(current);
|
|
1357
|
+
return cells.map(c => ({ ...c, code: c.code.replace(/\s+$/, '') }));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function evalLangToHljs(lang) {
|
|
1361
|
+
return lang === 'py' ? 'python' : lang === 'js' ? 'javascript' : lang === 'ts' ? 'typescript' : null;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function renderEval(name, args, result, ctx) {
|
|
1365
|
+
let html = toolHead('eval');
|
|
1366
|
+
if (typeof args.input !== 'string') {
|
|
1367
|
+
html += '<div class="tool-error">[missing input]</div>';
|
|
1368
|
+
} else {
|
|
1369
|
+
const cells = parseEvalCells(args.input);
|
|
1370
|
+
if (cells.length === 0) {
|
|
1371
|
+
html += codeBlock(args.input, 'python');
|
|
1372
|
+
} else {
|
|
1373
|
+
for (const cell of cells) {
|
|
1374
|
+
html += '<div class="tool-cell">';
|
|
1375
|
+
const titleParts = [];
|
|
1376
|
+
if (cell.title) titleParts.push(cell.title);
|
|
1377
|
+
titleParts.push(cell.lang);
|
|
1378
|
+
if (cell.attrs && cell.attrs.length) titleParts.push(...cell.attrs);
|
|
1379
|
+
html += '<div class="tool-cell-title">' + escapeHtml(titleParts.join(' · ')) + '</div>';
|
|
1380
|
+
html += codeBlock(cell.code, evalLangToHljs(cell.lang));
|
|
1381
|
+
html += '</div>';
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (result) {
|
|
1386
|
+
html += ctx.renderResultImages();
|
|
1387
|
+
const output = ctx.getResultText();
|
|
1388
|
+
if (output) html += formatExpandableOutput(output, 12);
|
|
1389
|
+
}
|
|
1390
|
+
return html;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function renderSearch(name, args, result, ctx) {
|
|
1394
|
+
const pattern = str(args.pattern);
|
|
1395
|
+
const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (args.path ? shortenPath(String(args.path)) : '.');
|
|
1396
|
+
const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
|
|
1397
|
+
let head = '<span class="tool-name">search</span> <span class="tool-pattern">/' + patHtml + '/</span>';
|
|
1398
|
+
head += ' <span class="tool-arg-key">in</span> <span class="tool-path">' + escapeHtml(paths) + '</span>';
|
|
1399
|
+
const badges = [];
|
|
1400
|
+
if (args.i) badges.push('i');
|
|
1401
|
+
if (args.skip) badges.push('skip=' + args.skip);
|
|
1402
|
+
if (args.gitignore === false) badges.push('no-gitignore');
|
|
1403
|
+
for (const b of badges) head += ' <span class="tool-badge">' + escapeHtml(b) + '</span>';
|
|
1404
|
+
let html = '<div class="tool-header">' + head + '</div>';
|
|
1405
|
+
if (result) {
|
|
1406
|
+
const output = ctx.getResultText();
|
|
1407
|
+
if (output) html += formatExpandableOutput(output, 12);
|
|
1408
|
+
}
|
|
1409
|
+
return html;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function renderRecipe(name, args, result, ctx) {
|
|
1413
|
+
const op = str(args.op) || '?';
|
|
1414
|
+
let html = toolHead('recipe', '<span class="tool-arg-val">' + escapeHtml(op) + '</span>');
|
|
1415
|
+
if (result) {
|
|
1416
|
+
html += ctx.renderResultImages();
|
|
1417
|
+
const output = ctx.getResultText();
|
|
1418
|
+
if (output) html += formatExpandableOutput(output, 10);
|
|
1419
|
+
}
|
|
1420
|
+
return html;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function renderIrc(name, args, result, ctx) {
|
|
1424
|
+
const op = str(args.op) || '?';
|
|
1425
|
+
const badges = [op];
|
|
1426
|
+
if (args.to) badges.push('to=' + args.to);
|
|
1427
|
+
if (args.awaitReply === false) badges.push('no-reply');
|
|
1428
|
+
let html = toolHead('irc', '', badges);
|
|
1429
|
+
if (args.message) html += '<div class="tool-output"><div>' + escapeHtml(String(args.message)) + '</div></div>';
|
|
1430
|
+
if (result) {
|
|
1431
|
+
const output = ctx.getResultText();
|
|
1432
|
+
if (output) html += formatExpandableOutput(output, 8);
|
|
1433
|
+
}
|
|
1434
|
+
return html;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1252
1437
|
|
|
1253
1438
|
function renderGenericTool(name, args, result, ctx) {
|
|
1254
1439
|
let html = toolHead(name);
|
|
@@ -1266,6 +1451,7 @@
|
|
|
1266
1451
|
|
|
1267
1452
|
const TOOL_RENDERERS = {
|
|
1268
1453
|
bash: renderBash,
|
|
1454
|
+
eval: renderEval,
|
|
1269
1455
|
js: renderJsLike,
|
|
1270
1456
|
python: renderJsLike,
|
|
1271
1457
|
notebook: renderJsLike,
|
|
@@ -1275,6 +1461,7 @@
|
|
|
1275
1461
|
ast_edit: renderAstEdit,
|
|
1276
1462
|
ast_grep: renderAstGrep,
|
|
1277
1463
|
grep: renderGrep,
|
|
1464
|
+
search: renderSearch,
|
|
1278
1465
|
find: renderFind,
|
|
1279
1466
|
lsp: renderLsp,
|
|
1280
1467
|
todo_write: renderTodoWrite,
|
|
@@ -1300,16 +1487,25 @@
|
|
|
1300
1487
|
poll: renderJob,
|
|
1301
1488
|
cancel_job: renderJob,
|
|
1302
1489
|
job: renderJob,
|
|
1490
|
+
recipe: renderRecipe,
|
|
1491
|
+
irc: renderIrc,
|
|
1303
1492
|
};
|
|
1304
1493
|
|
|
1305
1494
|
function renderToolCall(call) {
|
|
1306
1495
|
const result = findToolResult(call.id);
|
|
1307
1496
|
const isError = result?.isError || false;
|
|
1308
1497
|
const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
|
|
1309
|
-
const
|
|
1498
|
+
const rawArgs = call.arguments || {};
|
|
1499
|
+
const intent = typeof rawArgs._i === 'string' ? rawArgs._i.trim() : '';
|
|
1500
|
+
// Strip internal _i intent so renderers don't dump it as JSON.
|
|
1501
|
+
const args = {};
|
|
1502
|
+
for (const k of Object.keys(rawArgs)) {
|
|
1503
|
+
if (k !== '_i') args[k] = rawArgs[k];
|
|
1504
|
+
}
|
|
1310
1505
|
const name = call.name;
|
|
1311
1506
|
|
|
1312
1507
|
const ctx = {
|
|
1508
|
+
intent,
|
|
1313
1509
|
getResultText: () => {
|
|
1314
1510
|
if (!result) return '';
|
|
1315
1511
|
const textBlocks = result.content.filter(c => c.type === 'text');
|
|
@@ -1331,6 +1527,7 @@
|
|
|
1331
1527
|
|
|
1332
1528
|
const renderer = TOOL_RENDERERS[name] || renderGenericTool;
|
|
1333
1529
|
let html = '<div class="tool-execution ' + statusClass + '">';
|
|
1530
|
+
if (intent) html += '<div class="tool-intent">' + escapeHtml(intent) + '</div>';
|
|
1334
1531
|
try {
|
|
1335
1532
|
html += renderer(name, args, result, ctx);
|
|
1336
1533
|
} catch (err) {
|
|
@@ -1455,6 +1652,18 @@
|
|
|
1455
1652
|
return html;
|
|
1456
1653
|
}
|
|
1457
1654
|
|
|
1655
|
+
if (msg.role === 'developer') {
|
|
1656
|
+
let html = `<div class="user-message developer-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1657
|
+
const content = msg.content;
|
|
1658
|
+
const text = typeof content === 'string' ? content :
|
|
1659
|
+
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
1660
|
+
if (text.trim()) {
|
|
1661
|
+
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
|
|
1662
|
+
}
|
|
1663
|
+
html += '</div>';
|
|
1664
|
+
return html;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1458
1667
|
if (msg.role === 'assistant') {
|
|
1459
1668
|
let html = `<div class="assistant-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1460
1669
|
|
|
@@ -1557,7 +1766,7 @@
|
|
|
1557
1766
|
// ============================================================
|
|
1558
1767
|
|
|
1559
1768
|
function computeStats(entryList) {
|
|
1560
|
-
let userMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1769
|
+
let userMessages = 0, developerMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1561
1770
|
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
|
|
1562
1771
|
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1563
1772
|
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
@@ -1567,6 +1776,7 @@
|
|
|
1567
1776
|
if (entry.type === 'message') {
|
|
1568
1777
|
const msg = entry.message;
|
|
1569
1778
|
if (msg.role === 'user') userMessages++;
|
|
1779
|
+
if (msg.role === 'developer') developerMessages++;
|
|
1570
1780
|
if (msg.role === 'assistant') {
|
|
1571
1781
|
assistantMessages++;
|
|
1572
1782
|
if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model);
|
|
@@ -1594,7 +1804,7 @@
|
|
|
1594
1804
|
}
|
|
1595
1805
|
}
|
|
1596
1806
|
|
|
1597
|
-
return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
|
1807
|
+
return { userMessages, developerMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
|
1598
1808
|
}
|
|
1599
1809
|
|
|
1600
1810
|
const globalStats = computeStats(entries);
|
|
@@ -1610,6 +1820,7 @@
|
|
|
1610
1820
|
|
|
1611
1821
|
const msgParts = [];
|
|
1612
1822
|
if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`);
|
|
1823
|
+
if (globalStats.developerMessages) msgParts.push(`${globalStats.developerMessages} developer`);
|
|
1613
1824
|
if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`);
|
|
1614
1825
|
if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`);
|
|
1615
1826
|
if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`);
|
|
@@ -1638,11 +1849,12 @@
|
|
|
1638
1849
|
}
|
|
1639
1850
|
|
|
1640
1851
|
if (tools && tools.length > 0) {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
</div>
|
|
1852
|
+
const namesHtml = tools.map(t => `<span class="tool-name-chip">${escapeHtml(t.name)}</span>`).join('');
|
|
1853
|
+
const fullHtml = tools.map(t => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span></div>`).join('');
|
|
1854
|
+
html += `<div class="tools-list collapsed" onclick="this.classList.toggle('collapsed')">
|
|
1855
|
+
<div class="tools-header">Available Tools (${tools.length})</div>
|
|
1856
|
+
<div class="tools-collapsed">${namesHtml}</div>
|
|
1857
|
+
<div class="tools-content">${fullHtml}</div>
|
|
1646
1858
|
</div>`;
|
|
1647
1859
|
}
|
|
1648
1860
|
|
|
@@ -117,6 +117,31 @@ export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]>
|
|
|
117
117
|
// Path Resolution
|
|
118
118
|
// =============================================================================
|
|
119
119
|
|
|
120
|
+
const MANIFEST_ENTRY_INDEX_NAMES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a plugin manifest entry to a concrete loadable file path. Returns the
|
|
124
|
+
* file path itself when the entry points at a file, the matching index file when
|
|
125
|
+
* the entry points at a directory containing index.{ts,js,mjs,cjs}, and null
|
|
126
|
+
* when no entry exists at the joined path.
|
|
127
|
+
*/
|
|
128
|
+
function resolveManifestEntryFile(joined: string): string | null {
|
|
129
|
+
let stats: fs.Stats;
|
|
130
|
+
try {
|
|
131
|
+
stats = fs.statSync(joined);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (stats.isDirectory()) {
|
|
136
|
+
for (const name of MANIFEST_ENTRY_INDEX_NAMES) {
|
|
137
|
+
const candidate = path.join(joined, name);
|
|
138
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return joined;
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
/**
|
|
121
146
|
* Generic path resolver for plugin manifest entries (tools, hooks, commands, extensions).
|
|
122
147
|
* Handles both single-string and string[] base entries, plus feature-specific entries.
|
|
@@ -130,8 +155,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
130
155
|
if (base) {
|
|
131
156
|
const entries = Array.isArray(base) ? base : [base];
|
|
132
157
|
for (const entry of entries) {
|
|
133
|
-
const resolved = path.join(plugin.path, entry);
|
|
134
|
-
if (
|
|
158
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
159
|
+
if (resolved) {
|
|
135
160
|
paths.push(resolved);
|
|
136
161
|
}
|
|
137
162
|
}
|
|
@@ -146,8 +171,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
146
171
|
|
|
147
172
|
if (feat[key]) {
|
|
148
173
|
for (const entry of feat[key]) {
|
|
149
|
-
const resolved = path.join(plugin.path, entry);
|
|
150
|
-
if (
|
|
174
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
175
|
+
if (resolved) {
|
|
151
176
|
paths.push(resolved);
|
|
152
177
|
}
|
|
153
178
|
}
|
|
@@ -160,8 +185,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
160
185
|
|
|
161
186
|
if (feat[key]) {
|
|
162
187
|
for (const entry of feat[key]) {
|
|
163
|
-
const resolved = path.join(plugin.path, entry);
|
|
164
|
-
if (
|
|
188
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
189
|
+
if (resolved) {
|
|
165
190
|
paths.push(resolved);
|
|
166
191
|
}
|
|
167
192
|
}
|
|
@@ -28,6 +28,26 @@ export interface LoadSkillsResult {
|
|
|
28
28
|
warnings: SkillWarning[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
let activeSkills: readonly Skill[] = [];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Process-global snapshot of skills the active session loaded.
|
|
35
|
+
* Read by internal URL protocol handlers (skill://).
|
|
36
|
+
*/
|
|
37
|
+
export function getActiveSkills(): readonly Skill[] {
|
|
38
|
+
return activeSkills;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Replace the active skill snapshot. Called once per top-level session. */
|
|
42
|
+
export function setActiveSkills(value: readonly Skill[]): void {
|
|
43
|
+
activeSkills = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reset the active skill snapshot. Test-only. */
|
|
47
|
+
export function resetActiveSkillsForTests(): void {
|
|
48
|
+
activeSkills = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
export interface LoadSkillsFromDirOptions {
|
|
32
52
|
/** Directory to scan for skills */
|
|
33
53
|
dir: string;
|
|
@@ -6,3 +6,23 @@ export const RANGE_INTERIOR_HASH = "**";
|
|
|
6
6
|
|
|
7
7
|
/** Header marker introducing a new file section in multi-section input. */
|
|
8
8
|
export const FILE_HEADER_PREFIX = "@";
|
|
9
|
+
|
|
10
|
+
/** Optional patch envelope start marker; silently consumed when present. */
|
|
11
|
+
export const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
12
|
+
|
|
13
|
+
/** Optional patch envelope end marker; terminates parsing when encountered. */
|
|
14
|
+
export const END_PATCH_MARKER = "*** End Patch";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Recovery sentinel emitted by the agent loop when a contaminated
|
|
18
|
+
* `to=functions.edit` stream is truncated mid-call (see
|
|
19
|
+
* `docs/ERRATA-GPT5-HARMONY.md`). Behaves like `END_PATCH_MARKER` for
|
|
20
|
+
* parsing — terminates the line loop — and additionally surfaces a
|
|
21
|
+
* warning in the tool result so the model knows to re-issue any
|
|
22
|
+
* remaining edits.
|
|
23
|
+
*/
|
|
24
|
+
export const ABORT_MARKER = "*** Abort";
|
|
25
|
+
|
|
26
|
+
/** Warning text appended to the tool result when ABORT_MARKER terminates parsing. */
|
|
27
|
+
export const ABORT_WARNING =
|
|
28
|
+
"Tool stream truncated mid-call due to detected output corruption. Applied ops above are valid. Re-issue any remaining edits.";
|
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
start: section+
|
|
5
|
-
|
|
6
|
-
section: file_header line_op*
|
|
7
|
-
|
|
8
|
-
file_header: "@" path LF
|
|
1
|
+
start: begin_patch hunk+ end_patch
|
|
2
|
+
begin_patch: "*** Begin Patch" LF
|
|
3
|
+
end_patch: "*** End Patch" LF?
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| replace_op payload*
|
|
13
|
-
| delete_op
|
|
14
|
-
| blank
|
|
5
|
+
hunk: update_hunk
|
|
6
|
+
update_hunk: "@" filename LF line_op*
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
insert_after_op: "+" insert_target LF
|
|
18
|
-
replace_op: "=" range LF
|
|
19
|
-
delete_op: "-" range LF
|
|
20
|
-
payload: $HSEP$ line_text? LF
|
|
8
|
+
filename: /(.+)/
|
|
21
9
|
|
|
22
|
-
|
|
10
|
+
line_op: insert_before | insert_after | replace | delete | blank
|
|
11
|
+
insert_before: ("<" | "< ") anchor LF payload+
|
|
12
|
+
insert_after: ("+" | "+ ") anchor LF payload+
|
|
13
|
+
replace: ("=" | "= ") range LF payload*
|
|
14
|
+
delete: ("-" | "- ") range LF
|
|
15
|
+
payload: $HSEP$ /(.*)/ LF
|
|
16
|
+
blank: LF
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
anchor: LID | "EOF" | "BOF"
|
|
25
19
|
range: LID ".." LID
|
|
20
|
+
LID: /[1-9]\d*$HFMT$/
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
LID: /[1-9][0-9]*$HFMT$/
|
|
29
|
-
blank: LF
|
|
22
|
+
%import common.LF
|
package/src/hashline/hash.ts
CHANGED
|
@@ -137,48 +137,18 @@ export const HL_BODY_SEP = "|";
|
|
|
137
137
|
export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
|
|
138
138
|
|
|
139
139
|
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
|
|
140
|
-
const RE_STRUCTURAL_STRIP = /[\s{}]/g;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Bigram returned for lines that contain only whitespace and `{`/`}`.
|
|
144
|
-
* Picks the English ordinal suffix for the line number (`1` → `st`,
|
|
145
|
-
* `2` → `nd`, `3` → `rd`, `11`/`12`/`13` → `th`, else `th`) so the
|
|
146
|
-
* line digits + bigram BPE-merge into a single ordinal token (`1st`, `42nd`,
|
|
147
|
-
* `100th`, …). Brace-only lines therefore cost one token for the whole
|
|
148
|
-
* `LINE+ID` anchor instead of two.
|
|
149
|
-
*/
|
|
150
|
-
function structuralBigram(line: number): string {
|
|
151
|
-
const mod100 = line % 100;
|
|
152
|
-
if (mod100 >= 11 && mod100 <= 13) return "th";
|
|
153
|
-
switch (line % 10) {
|
|
154
|
-
case 1:
|
|
155
|
-
return "st";
|
|
156
|
-
case 2:
|
|
157
|
-
return "nd";
|
|
158
|
-
case 3:
|
|
159
|
-
return "rd";
|
|
160
|
-
default:
|
|
161
|
-
return "th";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
140
|
|
|
165
141
|
/**
|
|
166
142
|
* Compute a 2-character hash of a single line via xxHash32 mod 647 over
|
|
167
|
-
* {@link HL_BIGRAMS}. Lines
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* adjacent identical punctuation-only lines get distinct hashes; lines with
|
|
172
|
-
* significant content stay line-number-independent so a line is identifiable
|
|
173
|
-
* across small shifts.
|
|
143
|
+
* {@link HL_BIGRAMS}. Lines with no letter or digit mix the line number
|
|
144
|
+
* into the seed so adjacent identical punctuation-only lines (e.g. brace-only
|
|
145
|
+
* lines) get distinct hashes; lines with significant content stay
|
|
146
|
+
* line-number-independent so a line is identifiable across small shifts.
|
|
174
147
|
*
|
|
175
148
|
* The line input should not include a trailing newline.
|
|
176
149
|
*/
|
|
177
150
|
export function computeLineHash(idx: number, line: string): string {
|
|
178
151
|
line = line.replace(/\r/g, "").trimEnd();
|
|
179
|
-
if (line.replace(RE_STRUCTURAL_STRIP, "").length === 0) {
|
|
180
|
-
return structuralBigram(idx);
|
|
181
|
-
}
|
|
182
152
|
const seed = RE_SIGNIFICANT.test(line) ? 0 : idx;
|
|
183
153
|
return HL_BIGRAMS[Bun.hash.xxHash32(line, seed) % HL_BIGRAMS_COUNT];
|
|
184
154
|
}
|
package/src/hashline/input.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { FILE_HEADER_PREFIX } from "./constants";
|
|
2
|
+
import { ABORT_MARKER, BEGIN_PATCH_MARKER, END_PATCH_MARKER, FILE_HEADER_PREFIX } from "./constants";
|
|
3
3
|
import { HL_EDIT_SEP } from "./hash";
|
|
4
4
|
import type { SplitHashlineOptions } from "./types";
|
|
5
5
|
import { stripTrailingCarriageReturn } from "./utils";
|
|
@@ -38,10 +38,22 @@ function parseHashlineHeaderLine(line: string, cwd?: string): HashlineInputSecti
|
|
|
38
38
|
return { path: parsedPath, diff: "" };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function isPatchEnvelopeMarker(line: string): boolean {
|
|
42
|
+
const trimmed = line.trimEnd();
|
|
43
|
+
return trimmed === BEGIN_PATCH_MARKER || trimmed === END_PATCH_MARKER;
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
function stripLeadingBlankLines(input: string): string {
|
|
42
47
|
const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
|
|
43
48
|
const lines = stripped.split("\n");
|
|
44
|
-
while (lines.length > 0
|
|
49
|
+
while (lines.length > 0) {
|
|
50
|
+
const head = lines[0].replace(/\r$/, "");
|
|
51
|
+
if (head.trim().length === 0 || head.trimEnd() === BEGIN_PATCH_MARKER) {
|
|
52
|
+
lines.shift();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
45
57
|
return lines.join("\n");
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -96,6 +108,8 @@ export function splitHashlineInputs(input: string, options: SplitHashlineOptions
|
|
|
96
108
|
|
|
97
109
|
for (const rawLine of lines) {
|
|
98
110
|
const line = stripTrailingCarriageReturn(rawLine);
|
|
111
|
+
if (line.trimEnd() === END_PATCH_MARKER || line.trimEnd() === ABORT_MARKER) break;
|
|
112
|
+
if (isPatchEnvelopeMarker(line)) continue;
|
|
99
113
|
const header = parseHashlineHeaderLine(line, options.cwd);
|
|
100
114
|
if (header !== null) {
|
|
101
115
|
flush();
|
package/src/hashline/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RANGE_INTERIOR_HASH } from "./constants";
|
|
1
|
+
import { ABORT_MARKER, ABORT_WARNING, BEGIN_PATCH_MARKER, END_PATCH_MARKER, RANGE_INTERIOR_HASH } from "./constants";
|
|
2
2
|
import { describeAnchorExamples, HL_EDIT_SEP, HL_HASH_CAPTURE_RE_RAW } from "./hash";
|
|
3
3
|
import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
|
|
4
4
|
import { stripTrailingCarriageReturn } from "./utils";
|
|
@@ -118,6 +118,17 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
118
118
|
i++;
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
|
+
if (line === END_PATCH_MARKER) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (line === ABORT_MARKER) {
|
|
125
|
+
warnings.push(ABORT_WARNING);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if (line === BEGIN_PATCH_MARKER) {
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
121
132
|
if (line.startsWith(HL_EDIT_SEP)) {
|
|
122
133
|
throw new Error(`line ${lineNum}: payload line has no preceding +, <, or = operation.`);
|
|
123
134
|
}
|