@oh-my-pi/pi-coding-agent 14.5.11 → 14.5.13
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 +58 -0
- package/package.json +18 -10
- package/src/cli/jupyter-cli.ts +1 -1
- package/src/config/model-equivalence.ts +49 -16
- package/src/config/model-registry.ts +100 -25
- package/src/config/model-resolver.ts +29 -15
- package/src/config/settings-schema.ts +20 -6
- package/src/config/settings.ts +9 -8
- package/src/config.ts +9 -0
- package/src/eval/backend.ts +43 -0
- package/src/eval/eval.lark +43 -0
- package/src/eval/index.ts +5 -0
- package/src/eval/js/context-manager.ts +717 -0
- package/src/eval/js/executor.ts +131 -0
- package/src/eval/js/index.ts +46 -0
- package/src/eval/js/prelude.ts +2 -0
- package/src/eval/js/prelude.txt +84 -0
- package/src/eval/js/tool-bridge.ts +124 -0
- package/src/eval/parse.ts +337 -0
- package/src/{ipy → eval/py}/executor.ts +2 -180
- package/src/{ipy → eval/py}/gateway-coordinator.ts +4 -3
- package/src/eval/py/index.ts +58 -0
- package/src/{ipy → eval/py}/kernel.ts +5 -41
- package/src/{ipy → eval/py}/prelude.py +39 -227
- package/src/eval/types.ts +48 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +23 -17
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +9 -0
- package/src/lsp/index.ts +395 -0
- package/src/lsp/types.ts +15 -4
- package/src/main.ts +25 -14
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/command-controller.ts +28 -8
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/types.ts +3 -3
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/atom.md +3 -2
- package/src/prompts/tools/browser.md +61 -16
- package/src/prompts/tools/eval.md +92 -0
- package/src/prompts/tools/lsp.md +7 -3
- package/src/sdk.ts +45 -31
- package/src/session/agent-session.ts +44 -54
- package/src/session/messages.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +34 -66
- package/src/task/executor.ts +5 -9
- package/src/tools/browser/attach.ts +175 -0
- package/src/tools/browser/launch.ts +576 -0
- package/src/tools/browser/readable.ts +90 -0
- package/src/tools/browser/registry.ts +198 -0
- package/src/tools/browser/render.ts +212 -0
- package/src/tools/browser/tab-protocol.ts +101 -0
- package/src/tools/browser/tab-supervisor.ts +429 -0
- package/src/tools/browser/tab-worker-entry.ts +21 -0
- package/src/tools/browser/tab-worker.ts +1006 -0
- package/src/tools/browser.ts +231 -1567
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/{python.ts → eval.ts} +324 -315
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/index.ts +62 -100
- package/src/tools/plan-mode-guard.ts +27 -1
- package/src/tools/read.ts +0 -6
- package/src/tools/recipe/runners/pkg.ts +34 -32
- package/src/tools/renderers.ts +4 -2
- package/src/tools/resolve.ts +7 -2
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/tool-timeouts.ts +2 -2
- package/src/utils/markit.ts +15 -7
- package/src/utils/tools-manager.ts +5 -5
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +121 -39
- package/src/web/search/providers/gemini.ts +2 -2
- package/src/web/search/render.ts +2 -2
- package/src/ipy/modules.ts +0 -144
- package/src/prompts/tools/python.md +0 -57
- /package/src/{ipy → eval/py}/cancellation.ts +0 -0
- /package/src/{ipy → eval/py}/prelude.ts +0 -0
- /package/src/{ipy → eval/py}/runtime.ts +0 -0
package/src/lsp/client.ts
CHANGED
|
@@ -153,6 +153,15 @@ const CLIENT_CAPABILITIES = {
|
|
|
153
153
|
valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
|
|
154
154
|
},
|
|
155
155
|
},
|
|
156
|
+
fileOperations: {
|
|
157
|
+
dynamicRegistration: false,
|
|
158
|
+
willCreate: false,
|
|
159
|
+
didCreate: false,
|
|
160
|
+
willRename: true,
|
|
161
|
+
didRename: true,
|
|
162
|
+
willDelete: false,
|
|
163
|
+
didDelete: false,
|
|
164
|
+
},
|
|
156
165
|
},
|
|
157
166
|
experimental: {
|
|
158
167
|
snippetTextEdit: true,
|
package/src/lsp/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type LspServerStatus,
|
|
17
17
|
notifySaved,
|
|
18
18
|
refreshFile,
|
|
19
|
+
sendNotification,
|
|
19
20
|
sendRequest,
|
|
20
21
|
setIdleTimeout,
|
|
21
22
|
syncContent,
|
|
@@ -325,6 +326,61 @@ async function formatLocationWithContext(location: Location, cwd: string): Promi
|
|
|
325
326
|
}
|
|
326
327
|
return `${header}\n${context.map(lineText => ` ${lineText}`).join("\n")}`;
|
|
327
328
|
}
|
|
329
|
+
|
|
330
|
+
const MAX_RENAME_PAIRS = 1000;
|
|
331
|
+
|
|
332
|
+
interface FileRenamePair {
|
|
333
|
+
oldUri: string;
|
|
334
|
+
newUri: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Enumerate the {oldUri, newUri} pairs needed for an LSP willRenameFiles/didRenameFiles request.
|
|
339
|
+
* For files this is a single pair. For directories this walks every regular file underneath
|
|
340
|
+
* and produces a parallel pair anchored at the new directory root.
|
|
341
|
+
*/
|
|
342
|
+
async function enumerateRenamePairs(
|
|
343
|
+
source: string,
|
|
344
|
+
dest: string,
|
|
345
|
+
): Promise<{ pairs: FileRenamePair[]; directory: boolean; exceeded: boolean }> {
|
|
346
|
+
const stat = await fs.promises.stat(source);
|
|
347
|
+
if (!stat.isDirectory()) {
|
|
348
|
+
return {
|
|
349
|
+
pairs: [{ oldUri: fileToUri(source), newUri: fileToUri(dest) }],
|
|
350
|
+
directory: false,
|
|
351
|
+
exceeded: false,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const entries = await fs.promises.readdir(source, { recursive: true, withFileTypes: true });
|
|
355
|
+
const pairs: FileRenamePair[] = [];
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (!entry.isFile()) continue;
|
|
358
|
+
if (pairs.length >= MAX_RENAME_PAIRS) {
|
|
359
|
+
return { pairs, directory: true, exceeded: true };
|
|
360
|
+
}
|
|
361
|
+
const parent = entry.parentPath ?? source;
|
|
362
|
+
const absOld = path.join(parent, entry.name);
|
|
363
|
+
const rel = path.relative(source, absOld);
|
|
364
|
+
pairs.push({
|
|
365
|
+
oldUri: fileToUri(absOld),
|
|
366
|
+
newUri: fileToUri(path.join(dest, rel)),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return { pairs, directory: true, exceeded: false };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** True when an LSP error indicates the server doesn't implement the requested method. */
|
|
373
|
+
function isMethodNotFoundError(err: unknown): boolean {
|
|
374
|
+
if (!(err instanceof Error)) return false;
|
|
375
|
+
const msg = err.message.toLowerCase();
|
|
376
|
+
return (
|
|
377
|
+
msg.includes("method not found") ||
|
|
378
|
+
msg.includes("unhandled method") ||
|
|
379
|
+
msg.includes("not supported") ||
|
|
380
|
+
msg.includes("-32601")
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
328
384
|
async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
|
|
329
385
|
let output = `Restarted ${serverName}`;
|
|
330
386
|
const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
|
|
@@ -1312,6 +1368,345 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1312
1368
|
};
|
|
1313
1369
|
}
|
|
1314
1370
|
|
|
1371
|
+
if (action === "rename_file") {
|
|
1372
|
+
if (!file || !new_name) {
|
|
1373
|
+
return {
|
|
1374
|
+
content: [
|
|
1375
|
+
{
|
|
1376
|
+
type: "text",
|
|
1377
|
+
text: "Error: rename_file requires both `file` (source path) and `new_name` (destination path)",
|
|
1378
|
+
},
|
|
1379
|
+
],
|
|
1380
|
+
details: { action, success: false, request: params },
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const source = resolveToCwd(file, this.session.cwd);
|
|
1385
|
+
const dest = resolveToCwd(new_name, this.session.cwd);
|
|
1386
|
+
|
|
1387
|
+
if (source === dest) {
|
|
1388
|
+
return {
|
|
1389
|
+
content: [{ type: "text", text: "Error: source and destination paths are identical" }],
|
|
1390
|
+
details: { action, success: false, request: params },
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
let sourceStat: fs.Stats;
|
|
1395
|
+
try {
|
|
1396
|
+
sourceStat = await fs.promises.stat(source);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return {
|
|
1399
|
+
content: [
|
|
1400
|
+
{
|
|
1401
|
+
type: "text",
|
|
1402
|
+
text: `Error: source path does not exist: ${formatPathRelativeToCwd(source, this.session.cwd)}`,
|
|
1403
|
+
},
|
|
1404
|
+
],
|
|
1405
|
+
details: { action, success: false, request: params },
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
let destExists = false;
|
|
1410
|
+
try {
|
|
1411
|
+
await fs.promises.stat(dest);
|
|
1412
|
+
destExists = true;
|
|
1413
|
+
} catch {
|
|
1414
|
+
// expected: destination must not exist
|
|
1415
|
+
}
|
|
1416
|
+
if (destExists) {
|
|
1417
|
+
return {
|
|
1418
|
+
content: [
|
|
1419
|
+
{
|
|
1420
|
+
type: "text",
|
|
1421
|
+
text: `Error: destination already exists: ${formatPathRelativeToCwd(dest, this.session.cwd)}`,
|
|
1422
|
+
},
|
|
1423
|
+
],
|
|
1424
|
+
details: { action, success: false, request: params },
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const enumerated = await enumerateRenamePairs(source, dest);
|
|
1429
|
+
if (enumerated.exceeded) {
|
|
1430
|
+
return {
|
|
1431
|
+
content: [
|
|
1432
|
+
{
|
|
1433
|
+
type: "text",
|
|
1434
|
+
text: `Error: directory contains more than ${MAX_RENAME_PAIRS} files; rename in smaller batches to keep LSP edits accurate`,
|
|
1435
|
+
},
|
|
1436
|
+
],
|
|
1437
|
+
details: { action, success: false, request: params },
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
const { pairs } = enumerated;
|
|
1441
|
+
if (pairs.length === 0) {
|
|
1442
|
+
return {
|
|
1443
|
+
content: [{ type: "text", text: "Error: no files to rename" }],
|
|
1444
|
+
details: { action, success: false, request: params },
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const lspParams = { files: pairs };
|
|
1449
|
+
const servers = getLspServers(config);
|
|
1450
|
+
const respondingServers = new Set<string>();
|
|
1451
|
+
const perServerEdits: Array<{ serverName: string; edit: WorkspaceEdit }> = [];
|
|
1452
|
+
const serverNotes: string[] = [];
|
|
1453
|
+
|
|
1454
|
+
for (const [serverName, serverConfig] of servers) {
|
|
1455
|
+
throwIfAborted(signal);
|
|
1456
|
+
try {
|
|
1457
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1458
|
+
if (isProjectAwareLspServer(serverConfig)) {
|
|
1459
|
+
await waitForProjectLoaded(client, signal);
|
|
1460
|
+
}
|
|
1461
|
+
const result = (await sendRequest(
|
|
1462
|
+
client,
|
|
1463
|
+
"workspace/willRenameFiles",
|
|
1464
|
+
lspParams,
|
|
1465
|
+
signal,
|
|
1466
|
+
)) as WorkspaceEdit | null;
|
|
1467
|
+
respondingServers.add(serverName);
|
|
1468
|
+
if (result && (result.changes || result.documentChanges)) {
|
|
1469
|
+
perServerEdits.push({ serverName, edit: result });
|
|
1470
|
+
}
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1473
|
+
throw err;
|
|
1474
|
+
}
|
|
1475
|
+
if (!isMethodNotFoundError(err)) {
|
|
1476
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1477
|
+
serverNotes.push(` ${serverName}: ${msg}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const sourceLabel = formatPathRelativeToCwd(source, this.session.cwd);
|
|
1483
|
+
const destLabel = formatPathRelativeToCwd(dest, this.session.cwd);
|
|
1484
|
+
const fileCountLabel = sourceStat.isDirectory()
|
|
1485
|
+
? `${pairs.length} file${pairs.length !== 1 ? "s" : ""} under ${sourceLabel}`
|
|
1486
|
+
: sourceLabel;
|
|
1487
|
+
|
|
1488
|
+
const shouldApply = apply !== false;
|
|
1489
|
+
if (!shouldApply) {
|
|
1490
|
+
const lines: string[] = [];
|
|
1491
|
+
lines.push(`Rename preview: ${fileCountLabel} → ${destLabel}`);
|
|
1492
|
+
if (perServerEdits.length === 0) {
|
|
1493
|
+
lines.push(" No LSP edits would be applied");
|
|
1494
|
+
} else {
|
|
1495
|
+
for (const { serverName, edit } of perServerEdits) {
|
|
1496
|
+
const edits = formatWorkspaceEdit(edit, this.session.cwd);
|
|
1497
|
+
if (edits.length === 0) continue;
|
|
1498
|
+
lines.push(` ${serverName}:`);
|
|
1499
|
+
for (const e of edits) {
|
|
1500
|
+
lines.push(` ${e}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (serverNotes.length > 0) {
|
|
1505
|
+
lines.push(" Server notes:");
|
|
1506
|
+
lines.push(...serverNotes);
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1510
|
+
details: {
|
|
1511
|
+
action,
|
|
1512
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
1513
|
+
success: true,
|
|
1514
|
+
request: params,
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const summary: string[] = [];
|
|
1520
|
+
for (const { serverName, edit } of perServerEdits) {
|
|
1521
|
+
const applied = await applyWorkspaceEdit(edit, this.session.cwd);
|
|
1522
|
+
if (applied.length > 0) {
|
|
1523
|
+
summary.push(` ${serverName}:`);
|
|
1524
|
+
summary.push(...applied.map(line => ` ${line}`));
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
1529
|
+
await fs.promises.rename(source, dest);
|
|
1530
|
+
summary.push(` Renamed ${sourceLabel} → ${destLabel}`);
|
|
1531
|
+
|
|
1532
|
+
for (const [serverName, serverConfig] of servers) {
|
|
1533
|
+
try {
|
|
1534
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1535
|
+
for (const { oldUri } of pairs) {
|
|
1536
|
+
if (client.openFiles.has(oldUri)) {
|
|
1537
|
+
await sendNotification(client, "textDocument/didClose", {
|
|
1538
|
+
textDocument: { uri: oldUri },
|
|
1539
|
+
});
|
|
1540
|
+
client.openFiles.delete(oldUri);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
await sendNotification(client, "workspace/didRenameFiles", lspParams);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1546
|
+
throw err;
|
|
1547
|
+
}
|
|
1548
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1549
|
+
serverNotes.push(` ${serverName}: ${msg}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (serverNotes.length > 0) {
|
|
1554
|
+
summary.push(" Server notes:");
|
|
1555
|
+
summary.push(...serverNotes);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const header = `Renamed ${fileCountLabel} → ${destLabel}`;
|
|
1559
|
+
return {
|
|
1560
|
+
content: [{ type: "text", text: `${header}\n${summary.join("\n")}` }],
|
|
1561
|
+
details: {
|
|
1562
|
+
action,
|
|
1563
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
1564
|
+
success: true,
|
|
1565
|
+
request: params,
|
|
1566
|
+
},
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (action === "capabilities") {
|
|
1571
|
+
let serverList: Array<[string, ServerConfig]>;
|
|
1572
|
+
if (file && file !== "*") {
|
|
1573
|
+
const resolved = resolveToCwd(file, this.session.cwd);
|
|
1574
|
+
serverList = getLspServersForFile(config, resolved);
|
|
1575
|
+
if (serverList.length === 0) {
|
|
1576
|
+
return {
|
|
1577
|
+
content: [{ type: "text", text: "No language server found for this file" }],
|
|
1578
|
+
details: { action, success: false, request: params },
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
serverList = getLspServers(config);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (serverList.length === 0) {
|
|
1586
|
+
return {
|
|
1587
|
+
content: [{ type: "text", text: "No language servers configured" }],
|
|
1588
|
+
details: { action, success: false, request: params },
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const sections: string[] = [];
|
|
1593
|
+
const respondingServers = new Set<string>();
|
|
1594
|
+
for (const [serverName, serverConfig] of serverList) {
|
|
1595
|
+
throwIfAborted(signal);
|
|
1596
|
+
try {
|
|
1597
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1598
|
+
respondingServers.add(serverName);
|
|
1599
|
+
const caps = client.serverCapabilities ?? {};
|
|
1600
|
+
sections.push(`${serverName}:`);
|
|
1601
|
+
sections.push(` capabilities: ${JSON.stringify(caps, null, 2).split("\n").join("\n ")}`);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1604
|
+
throw err;
|
|
1605
|
+
}
|
|
1606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1607
|
+
sections.push(`${serverName}: failed to start (${msg})`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return {
|
|
1612
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
1613
|
+
details: {
|
|
1614
|
+
action,
|
|
1615
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
1616
|
+
success: true,
|
|
1617
|
+
request: params,
|
|
1618
|
+
},
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
if (action === "request") {
|
|
1623
|
+
const method = query?.trim();
|
|
1624
|
+
if (!method) {
|
|
1625
|
+
return {
|
|
1626
|
+
content: [
|
|
1627
|
+
{
|
|
1628
|
+
type: "text",
|
|
1629
|
+
text: "Error: action=request requires `query` to specify the LSP method name (e.g., 'rust-analyzer/expandMacro')",
|
|
1630
|
+
},
|
|
1631
|
+
],
|
|
1632
|
+
details: { action, success: false, request: params },
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
let chosenServer: [string, ServerConfig] | null = null;
|
|
1637
|
+
let resolvedTarget: string | null = null;
|
|
1638
|
+
if (file && file !== "*") {
|
|
1639
|
+
resolvedTarget = resolveToCwd(file, this.session.cwd);
|
|
1640
|
+
chosenServer = getLspServerForFile(config, resolvedTarget);
|
|
1641
|
+
if (!chosenServer) {
|
|
1642
|
+
return {
|
|
1643
|
+
content: [{ type: "text", text: "No language server found for this file" }],
|
|
1644
|
+
details: { action, success: false, request: params },
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
} else {
|
|
1648
|
+
const all = getLspServers(config);
|
|
1649
|
+
if (all.length === 0) {
|
|
1650
|
+
return {
|
|
1651
|
+
content: [{ type: "text", text: "No language servers configured" }],
|
|
1652
|
+
details: { action, success: false, request: params },
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
chosenServer = all[0];
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const [chosenName, chosenConfig] = chosenServer;
|
|
1659
|
+
let requestParams: unknown;
|
|
1660
|
+
if (params.payload !== undefined) {
|
|
1661
|
+
try {
|
|
1662
|
+
requestParams = JSON.parse(params.payload);
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1665
|
+
return {
|
|
1666
|
+
content: [{ type: "text", text: `Error: invalid JSON in payload: ${msg}` }],
|
|
1667
|
+
details: { action, serverName: chosenName, success: false, request: params },
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
} else if (resolvedTarget) {
|
|
1671
|
+
const uri = fileToUri(resolvedTarget);
|
|
1672
|
+
if (line !== undefined) {
|
|
1673
|
+
const character = await resolveSymbolColumn(resolvedTarget, line, symbol);
|
|
1674
|
+
requestParams = { textDocument: { uri }, position: { line: line - 1, character } };
|
|
1675
|
+
} else {
|
|
1676
|
+
requestParams = { textDocument: { uri } };
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
requestParams = {};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
try {
|
|
1683
|
+
const client = await getOrCreateClient(chosenConfig, this.session.cwd);
|
|
1684
|
+
if (resolvedTarget) {
|
|
1685
|
+
await ensureFileOpen(client, resolvedTarget, signal);
|
|
1686
|
+
}
|
|
1687
|
+
const result = await sendRequest(client, method, requestParams, signal);
|
|
1688
|
+
const formatted =
|
|
1689
|
+
result === null || result === undefined
|
|
1690
|
+
? "null"
|
|
1691
|
+
: typeof result === "string"
|
|
1692
|
+
? result
|
|
1693
|
+
: JSON.stringify(result, null, 2);
|
|
1694
|
+
return {
|
|
1695
|
+
content: [{ type: "text", text: `${chosenName} ← ${method}:\n${formatted}` }],
|
|
1696
|
+
details: { action, serverName: chosenName, success: true, request: params },
|
|
1697
|
+
};
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1700
|
+
throw new ToolAbortError();
|
|
1701
|
+
}
|
|
1702
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1703
|
+
return {
|
|
1704
|
+
content: [{ type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}` }],
|
|
1705
|
+
details: { action, serverName: chosenName, success: false, request: params },
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1315
1710
|
// `*` means workspace scope for symbols/reload; other actions need a concrete file.
|
|
1316
1711
|
const isWorkspace = file === "*";
|
|
1317
1712
|
const requiresFile = !file && action !== "reload";
|
package/src/lsp/types.ts
CHANGED
|
@@ -15,21 +15,32 @@ export const lspSchema = Type.Object({
|
|
|
15
15
|
"hover",
|
|
16
16
|
"symbols",
|
|
17
17
|
"rename",
|
|
18
|
+
"rename_file",
|
|
18
19
|
"code_actions",
|
|
19
20
|
"type_definition",
|
|
20
21
|
"implementation",
|
|
21
22
|
"status",
|
|
22
23
|
"reload",
|
|
24
|
+
"capabilities",
|
|
25
|
+
"request",
|
|
23
26
|
],
|
|
24
27
|
{ description: "LSP operation" },
|
|
25
28
|
),
|
|
26
|
-
file: Type.Optional(Type.String({ description: "File path" })),
|
|
29
|
+
file: Type.Optional(Type.String({ description: "File path or source path for rename_file" })),
|
|
27
30
|
line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
|
|
28
31
|
symbol: Type.Optional(Type.String({ description: "Symbol/substring to locate on the line" })),
|
|
29
|
-
query: Type.Optional(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
query: Type.Optional(
|
|
33
|
+
Type.String({ description: "Search query, code-action selector, or LSP method name for action=request" }),
|
|
34
|
+
),
|
|
35
|
+
new_name: Type.Optional(Type.String({ description: "New name for rename, or destination path for rename_file" })),
|
|
36
|
+
apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true for rename/rename_file)" })),
|
|
32
37
|
timeout: Type.Optional(Type.Number({ description: "Request timeout in seconds" })),
|
|
38
|
+
payload: Type.Optional(
|
|
39
|
+
Type.String({
|
|
40
|
+
description:
|
|
41
|
+
"JSON-encoded params for action=request. When omitted, params are auto-built from file/line/symbol.",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
33
44
|
});
|
|
34
45
|
|
|
35
46
|
export type LspParams = Static<typeof lspSchema>;
|
package/src/main.ts
CHANGED
|
@@ -242,6 +242,11 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
const lastVersion = settings.get("lastChangelogVersion");
|
|
245
|
+
if (lastVersion === VERSION) {
|
|
246
|
+
// Steady state: user already saw the current version's changelog. Skip the file read + parse.
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
245
250
|
const changelogPath = getChangelogPath();
|
|
246
251
|
const entries = await parseChangelog(changelogPath);
|
|
247
252
|
|
|
@@ -615,8 +620,19 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
615
620
|
process.exit(1);
|
|
616
621
|
}
|
|
617
622
|
|
|
623
|
+
// Kick off plugin-root preload in parallel with the remaining startup work.
|
|
624
|
+
// Awaited later (before extension/skill discovery in createAgentSession needs it).
|
|
625
|
+
const home = os.homedir();
|
|
626
|
+
const pluginPreloadPromise =
|
|
627
|
+
parsedArgs.pluginDirs && parsedArgs.pluginDirs.length > 0
|
|
628
|
+
? logger.time("injectPluginDirRoots", injectPluginDirRoots, home, parsedArgs.pluginDirs, getProjectDir())
|
|
629
|
+
: logger.time("preloadPluginRoots", preloadPluginRoots, home, getProjectDir());
|
|
630
|
+
// Mark the promise as handled so a synchronous failure does not surface as an unhandled-rejection
|
|
631
|
+
// warning before we reach the await site below.
|
|
632
|
+
pluginPreloadPromise.catch(() => {});
|
|
633
|
+
|
|
618
634
|
const cwd = getProjectDir();
|
|
619
|
-
await logger.time("settings:init", Settings.init, { cwd });
|
|
635
|
+
const settingsInstance = await logger.time("settings:init", Settings.init, { cwd });
|
|
620
636
|
if (parsedArgs.mode === "rpc") {
|
|
621
637
|
applyRpcDefaultSettingOverrides();
|
|
622
638
|
}
|
|
@@ -647,9 +663,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
647
663
|
const mode = parsedArgs.mode || "text";
|
|
648
664
|
|
|
649
665
|
// Initialize discovery system with settings for provider persistence
|
|
650
|
-
logger.time("initializeWithSettings");
|
|
651
|
-
initializeWithSettings(settings);
|
|
652
|
-
modelRegistry.refreshInBackground();
|
|
666
|
+
logger.time("initializeWithSettings", initializeWithSettings, settings);
|
|
653
667
|
|
|
654
668
|
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
655
669
|
const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
|
|
@@ -706,13 +720,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
706
720
|
sessionManager = await SessionManager.open(selectedPath);
|
|
707
721
|
}
|
|
708
722
|
|
|
709
|
-
|
|
710
|
-
const home = os.homedir();
|
|
711
|
-
if (parsedArgs.pluginDirs && parsedArgs.pluginDirs.length > 0) {
|
|
712
|
-
await logger.time("injectPluginDirRoots", injectPluginDirRoots, home, parsedArgs.pluginDirs!, getProjectDir());
|
|
713
|
-
} else {
|
|
714
|
-
await logger.time("preloadPluginRoots", preloadPluginRoots, home, getProjectDir());
|
|
715
|
-
}
|
|
723
|
+
await pluginPreloadPromise;
|
|
716
724
|
|
|
717
725
|
// Background marketplace auto-update — never blocks startup.
|
|
718
726
|
const autoUpdate = settings.get("marketplace.autoUpdate");
|
|
@@ -759,6 +767,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
759
767
|
sessionOptions.authStorage = authStorage;
|
|
760
768
|
sessionOptions.modelRegistry = modelRegistry;
|
|
761
769
|
sessionOptions.hasUI = isInteractive;
|
|
770
|
+
sessionOptions.settings = settingsInstance;
|
|
762
771
|
|
|
763
772
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
764
773
|
if (parsedArgs.apiKey) {
|
|
@@ -778,7 +787,10 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
778
787
|
createAgentSession,
|
|
779
788
|
sessionOptions,
|
|
780
789
|
);
|
|
781
|
-
|
|
790
|
+
// Kick off background model discovery only after createAgentSession finishes its parallel
|
|
791
|
+
// discovery arms; running these concurrently contends for the event loop and stretches
|
|
792
|
+
// every parallel arm by ~30ms.
|
|
793
|
+
modelRegistry.refreshInBackground();
|
|
782
794
|
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
|
783
795
|
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
784
796
|
}
|
|
@@ -856,8 +868,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
856
868
|
await runAcpMode(session, createAcpSession);
|
|
857
869
|
} else if (isInteractive) {
|
|
858
870
|
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
|
|
859
|
-
logger.time("main:getChangelogForDisplay");
|
|
860
|
-
const changelogMarkdown = await getChangelogForDisplay(parsedArgs);
|
|
871
|
+
const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
|
|
861
872
|
|
|
862
873
|
const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
|
|
863
874
|
if (scopedModelsForDisplay.length > 0) {
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* by providing authorization URL, token URL, and client credentials.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { OAuthController, OAuthCredentials } from "@oh-my-pi/pi-ai";
|
|
9
8
|
import type { OAuthCallbackFlowOptions } from "@oh-my-pi/pi-ai/utils/oauth/callback-server";
|
|
10
9
|
import { OAuthCallbackFlow } from "@oh-my-pi/pi-ai/utils/oauth/callback-server";
|
|
10
|
+
import type { OAuthController, OAuthCredentials } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_PORT = 3000;
|
|
13
13
|
const CALLBACK_PATH = "/callback";
|
package/src/memories/index.ts
CHANGED
|
@@ -544,7 +544,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean {
|
|
|
544
544
|
}
|
|
545
545
|
if (role !== "toolResult") return false;
|
|
546
546
|
const toolName = (message as { toolName?: string }).toolName;
|
|
547
|
-
if (toolName === "bash" || toolName === "
|
|
547
|
+
if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "search") {
|
|
548
548
|
const text = extractMessageText(message);
|
|
549
549
|
return text.length > 0 && text.length <= 32_000;
|
|
550
550
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Component for displaying user-initiated
|
|
3
|
-
* Shares the same kernel session as the agent's
|
|
2
|
+
* Component for displaying user-initiated eval execution with streaming output.
|
|
3
|
+
* Shares the same kernel session as the agent's eval tool.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
@@ -13,7 +13,9 @@ import { truncateToVisualLines } from "./visual-truncate";
|
|
|
13
13
|
const PREVIEW_LINES = 20;
|
|
14
14
|
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
15
15
|
|
|
16
|
-
export
|
|
16
|
+
export type EvalExecutionLanguage = "python" | "js";
|
|
17
|
+
|
|
18
|
+
export class EvalExecutionComponent extends Container {
|
|
17
19
|
#outputLines: string[] = [];
|
|
18
20
|
#status: "running" | "complete" | "cancelled" | "error" = "running";
|
|
19
21
|
#exitCode: number | undefined = undefined;
|
|
@@ -22,10 +24,14 @@ export class PythonExecutionComponent extends Container {
|
|
|
22
24
|
#expanded = false;
|
|
23
25
|
#contentContainer: Container;
|
|
24
26
|
|
|
27
|
+
#highlightLang(): "python" | "javascript" {
|
|
28
|
+
return this.language === "js" ? "javascript" : "python";
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
#formatHeader(colorKey: "dim" | "pythonMode"): Text {
|
|
26
32
|
const prompt = theme.fg(colorKey, theme.bold(">>>"));
|
|
27
33
|
const continuation = theme.fg(colorKey, " ");
|
|
28
|
-
const codeLines = highlightCode(this.code,
|
|
34
|
+
const codeLines = highlightCode(this.code, this.#highlightLang());
|
|
29
35
|
const headerLines = codeLines.map((line, index) =>
|
|
30
36
|
index === 0 ? `${prompt} ${line}` : `${continuation}${line}`,
|
|
31
37
|
);
|
|
@@ -36,6 +42,7 @@ export class PythonExecutionComponent extends Container {
|
|
|
36
42
|
private readonly code: string,
|
|
37
43
|
ui: TUI,
|
|
38
44
|
private readonly excludeFromContext = false,
|
|
45
|
+
private readonly language: EvalExecutionLanguage = "python",
|
|
39
46
|
) {
|
|
40
47
|
super();
|
|
41
48
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOAuthProviders } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
2
2
|
import { Container, getKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { theme } from "../../modes/theme/theme";
|
|
4
4
|
import { openPath } from "../../utils/open";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getOAuthProviders
|
|
1
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
2
|
+
import type { OAuthProviderInfo } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
2
3
|
import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { theme } from "../../modes/theme/theme";
|
|
4
5
|
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
|
@@ -18,6 +18,7 @@ import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "..
|
|
|
18
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
19
19
|
import { theme } from "../../modes/theme/theme";
|
|
20
20
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
21
|
+
import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
|
|
21
22
|
import {
|
|
22
23
|
formatArgsInline,
|
|
23
24
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
29
30
|
renderJsonTreeLines,
|
|
30
31
|
} from "../../tools/json-tree";
|
|
31
|
-
import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python";
|
|
32
32
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
33
33
|
import { toolRenderers } from "../../tools/renderers";
|
|
34
34
|
import { renderStatusLine } from "../../tui";
|
|
@@ -668,12 +668,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
668
668
|
context.expanded = this.#expanded;
|
|
669
669
|
context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
|
|
670
670
|
context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 3600);
|
|
671
|
-
} else if (this.#toolName === "
|
|
671
|
+
} else if (this.#toolName === "eval" && this.#result) {
|
|
672
672
|
const output = this.#getTextOutput().trimEnd();
|
|
673
673
|
context.output = output;
|
|
674
674
|
context.expanded = this.#expanded;
|
|
675
|
-
context.previewLines =
|
|
676
|
-
context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 600);
|
|
675
|
+
context.previewLines = EVAL_DEFAULT_PREVIEW_LINES;
|
|
677
676
|
} else if (isEditLikeToolName(this.#toolName)) {
|
|
678
677
|
context.editMode = this.#editMode;
|
|
679
678
|
const previews = this.#editDiffPreview;
|