zidane 5.6.14 → 5.7.4
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 +3 -1
- package/dist/{agent-ClkpElCZ.d.ts → agent-BNS2nx_T.d.ts} +535 -15
- package/dist/agent-BNS2nx_T.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +4 -0
- package/dist/chat/pure.js +3 -0
- package/dist/chat.d.ts +31 -661
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -3
- package/dist/chat.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
- package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-DnfNoj16.js +574 -0
- package/dist/edit-utils-DnfNoj16.js.map +1 -0
- package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
- package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
- package/dist/fetch-url-CPxfiXDa.js +518 -0
- package/dist/fetch-url-CPxfiXDa.js.map +1 -0
- package/dist/image-sniff-B7uFSNO1.js +90 -0
- package/dist/image-sniff-B7uFSNO1.js.map +1 -0
- package/dist/{index-CbS75MD3.d.ts → index-CZOwAJIX.d.ts} +2 -2
- package/dist/index-CZOwAJIX.d.ts.map +1 -0
- package/dist/{index-CTDMMdIy.d.ts → index-Ck_AWt8P.d.ts} +3 -4
- package/dist/index-Ck_AWt8P.d.ts.map +1 -0
- package/dist/{index-v3Tzobqr.d.ts → index-KiS7w0dC.d.ts} +3 -3
- package/dist/index-KiS7w0dC.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-DM1UcKeQ.js → interpolate-TySiqKzc.js} +23 -23
- package/dist/{interpolate-DM1UcKeQ.js.map → interpolate-TySiqKzc.js.map} +1 -1
- package/dist/{login-7tHcckmX.js → login-BDeqENSe.js} +7 -58
- package/dist/login-BDeqENSe.js.map +1 -0
- package/dist/{mcp-DGeB7-3D.js → mcp-Kqzz-Rs_.js} +8 -6
- package/dist/mcp-Kqzz-Rs_.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{messages-Dym8S_YH.js → messages-CvRQTdbR.js} +118 -39
- package/dist/messages-CvRQTdbR.js.map +1 -0
- package/dist/{presets-w9Px_aAm.js → presets-JuOnSI-i.js} +2 -2
- package/dist/{presets-w9Px_aAm.js.map → presets-JuOnSI-i.js.map} +1 -1
- package/dist/presets.d.ts +3 -3
- package/dist/presets.js +1 -1
- package/dist/{providers-beXyD9W9.js → providers-h4HJPbbv.js} +485 -31
- package/dist/providers-h4HJPbbv.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BRIsmBSY.js → session-BzLou2_-.js} +2 -2
- package/dist/{session-BRIsmBSY.js.map → session-BzLou2_-.js.map} +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{stats-Lc3zL3RM.js → stats-DAKBEKjc.js} +12 -2
- package/dist/stats-DAKBEKjc.js.map +1 -0
- package/dist/{stdio-loader-EVAF5KlU.js → stdio-loader-Ce68wUmM.js} +4 -4
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +70 -0
- package/dist/tools/fetch-url.d.ts.map +1 -0
- package/dist/tools/fetch-url.js +2 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +190 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/{tools-DhrLrOEr.js → tools-BGtJK0vo.js} +1368 -421
- package/dist/tools-BGtJK0vo.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +1 -1
- package/dist/{turn-operations-UAkOjO-u.js → transcript-anchors-BTSZAPVc.js} +147 -2713
- package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
- package/dist/{transcript-anchors-D0TR6djV.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
- package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
- package/dist/tui.d.ts +58 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1349 -422
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-CCHfR9eC.js +1938 -0
- package/dist/turn-operations-CCHfR9eC.js.map +1 -0
- package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
- package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
- package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
- package/dist/types-BPw_i5vb.js.map +1 -0
- package/dist/{types-KukEp-mi.d.ts → types-CEAMIUXw.d.ts} +1 -1
- package/dist/types-CEAMIUXw.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +3 -3
- package/docs/CHAT.md +53 -6
- package/docs/SKILL.md +3 -0
- package/docs/TUI.md +7 -0
- package/package.json +18 -2
- package/dist/agent-ClkpElCZ.d.ts.map +0 -1
- package/dist/index-CTDMMdIy.d.ts.map +0 -1
- package/dist/index-CbS75MD3.d.ts.map +0 -1
- package/dist/index-v3Tzobqr.d.ts.map +0 -1
- package/dist/login-7tHcckmX.js.map +0 -1
- package/dist/mcp-DGeB7-3D.js.map +0 -1
- package/dist/messages-Dym8S_YH.js.map +0 -1
- package/dist/providers-beXyD9W9.js.map +0 -1
- package/dist/stats-Lc3zL3RM.js.map +0 -1
- package/dist/stdio-loader-EVAF5KlU.js.map +0 -1
- package/dist/tools-DhrLrOEr.js.map +0 -1
- package/dist/transcript-anchors-D0TR6djV.d.ts.map +0 -1
- package/dist/turn-operations-UAkOjO-u.js.map +0 -1
- package/dist/types-KukEp-mi.d.ts.map +0 -1
- package/dist/types-oKPBdCmL.js.map +0 -1
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { G as modelSupportsReasoning, K as modelsForDescriptor, P as BUILTIN_PROVIDERS, R as credKeyOf, X as restoreModelOptions, _ as shell, a as multiEdit, c as grep, i as readFile$1, l as glob$1, n as createSpawnTool, o as listFiles, t as writeFile$1, u as edit, z as effectiveContextWindow } from "./tools-BGtJK0vo.js";
|
|
2
|
+
import { d as shortId, o as fmtTokens, u as previewLine } from "./edit-utils-DnfNoj16.js";
|
|
3
|
+
import { u as writeFileAtomic } from "./providers-h4HJPbbv.js";
|
|
4
|
+
import { l as errorMessage } from "./errors-CoQnKRf1.js";
|
|
5
|
+
import { O as joinSystemPrompt } from "./messages-CvRQTdbR.js";
|
|
6
|
+
import { r as toolResultToText } from "./types-BPw_i5vb.js";
|
|
7
|
+
import { a as discoverSkills } from "./interpolate-TySiqKzc.js";
|
|
8
|
+
import { r as normalizeMcpServers, t as connectMcpServers } from "./mcp-Kqzz-Rs_.js";
|
|
9
|
+
import { r as formatTokenUsage } from "./stats-DAKBEKjc.js";
|
|
10
|
+
import { n as definePreset, t as composePresets } from "./presets-JuOnSI-i.js";
|
|
11
|
+
import { r as fetchUrl } from "./fetch-url-CPxfiXDa.js";
|
|
12
|
+
import { webSearch } from "./tools/web-search.js";
|
|
13
|
+
import { D as extractEditPayload, I as parseEditOutcomesFromResult, J as collectReferences, Y as findActiveTrigger, q as applyInsert, u as ownerOf } from "./turn-operations-CCHfR9eC.js";
|
|
10
14
|
import { createRequire } from "node:module";
|
|
11
15
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
12
|
-
import {
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
16
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
14
17
|
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
15
20
|
import { readdir, stat, writeFile } from "node:fs/promises";
|
|
16
|
-
import {
|
|
17
|
-
import { getModel, getModels } from "@earendil-works/pi-ai";
|
|
18
|
-
import { createServer } from "node:http";
|
|
19
|
-
import { refreshAnthropicToken, refreshOpenAICodexToken } from "@earendil-works/pi-ai/oauth";
|
|
20
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
21
22
|
import { createGunzip } from "node:zlib";
|
|
22
23
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
23
24
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -889,7 +890,9 @@ const READ_ONLY_TOOLS = {
|
|
|
889
890
|
readFile: readFile$1,
|
|
890
891
|
listFiles,
|
|
891
892
|
glob: glob$1,
|
|
892
|
-
grep
|
|
893
|
+
grep,
|
|
894
|
+
webSearch,
|
|
895
|
+
fetchUrl
|
|
893
896
|
};
|
|
894
897
|
/** Full build-mode tool slice — read + write + shell + spawn. */
|
|
895
898
|
const BUILD_TOOLS = {
|
|
@@ -900,7 +903,9 @@ const BUILD_TOOLS = {
|
|
|
900
903
|
edit,
|
|
901
904
|
multiEdit,
|
|
902
905
|
glob: glob$1,
|
|
903
|
-
grep
|
|
906
|
+
grep,
|
|
907
|
+
webSearch,
|
|
908
|
+
fetchUrl
|
|
904
909
|
};
|
|
905
910
|
/**
|
|
906
911
|
* Tools the chat layer never persists to disk, regardless of output size.
|
|
@@ -1012,7 +1017,7 @@ const SHARED_BEHAVIOR = {
|
|
|
1012
1017
|
const BUILD_AGENT = {
|
|
1013
1018
|
id: "build",
|
|
1014
1019
|
label: "Build",
|
|
1015
|
-
description: "full tool access — read, write, edit, shell, and spawn subagents",
|
|
1020
|
+
description: "full tool access — read, write, edit, shell, web search, and spawn subagents",
|
|
1016
1021
|
accent: "accent",
|
|
1017
1022
|
preset: composePresets(definePreset({
|
|
1018
1023
|
name: "build",
|
|
@@ -1033,7 +1038,7 @@ const BUILD_AGENT = {
|
|
|
1033
1038
|
const PLAN_AGENT = {
|
|
1034
1039
|
id: "plan",
|
|
1035
1040
|
label: "Plan",
|
|
1036
|
-
description: "read-only — explore, analyze, and propose without modifying anything",
|
|
1041
|
+
description: "read-only — explore, analyze, search the web, and propose without modifying anything",
|
|
1037
1042
|
accent: "model",
|
|
1038
1043
|
preset: definePreset({
|
|
1039
1044
|
name: "plan",
|
|
@@ -1301,764 +1306,6 @@ function readIfPresent(path, source) {
|
|
|
1301
1306
|
};
|
|
1302
1307
|
}
|
|
1303
1308
|
//#endregion
|
|
1304
|
-
//#region src/chat/oauth-page/pkce.ts
|
|
1305
|
-
/**
|
|
1306
|
-
* PKCE helper. Inlined here (rather than imported from pi-ai) because
|
|
1307
|
-
* `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want
|
|
1308
|
-
* to reach into `node_modules/.../utils/oauth/pkce.js` — that's an
|
|
1309
|
-
* implementation-detail path that breaks on minor upstream rearrangements.
|
|
1310
|
-
*
|
|
1311
|
-
* Web Crypto API only. Works under Bun + Node 22+ without any node:crypto
|
|
1312
|
-
* import (matches pi-ai's own behavior). Output is base64url per RFC 7636.
|
|
1313
|
-
*/
|
|
1314
|
-
function base64UrlEncode(bytes) {
|
|
1315
|
-
let binary = "";
|
|
1316
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1317
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1318
|
-
}
|
|
1319
|
-
async function generatePkce() {
|
|
1320
|
-
const verifierBytes = new Uint8Array(32);
|
|
1321
|
-
crypto.getRandomValues(verifierBytes);
|
|
1322
|
-
const verifier = base64UrlEncode(verifierBytes);
|
|
1323
|
-
const data = new TextEncoder().encode(verifier);
|
|
1324
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1325
|
-
return {
|
|
1326
|
-
verifier,
|
|
1327
|
-
challenge: base64UrlEncode(new Uint8Array(hashBuffer))
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
//#endregion
|
|
1331
|
-
//#region src/chat/oauth-page/server.ts
|
|
1332
|
-
const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1";
|
|
1333
|
-
function buildRequestHandler(opts, pending) {
|
|
1334
|
-
return (req, res) => {
|
|
1335
|
-
try {
|
|
1336
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
1337
|
-
if (url.pathname !== opts.path) {
|
|
1338
|
-
respondError(res, 404, opts, "Callback route not found.");
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
const error = url.searchParams.get("error");
|
|
1342
|
-
if (error) {
|
|
1343
|
-
respondError(res, 400, opts, `${opts.providerName} authentication did not complete.`, `Error: ${error}`);
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
const code = url.searchParams.get("code");
|
|
1347
|
-
const state = url.searchParams.get("state");
|
|
1348
|
-
if (!code || !state) {
|
|
1349
|
-
respondError(res, 400, opts, "Missing code or state parameter.");
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
if (state !== opts.expectedState) {
|
|
1353
|
-
respondError(res, 400, opts, "State mismatch.");
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1357
|
-
res.end(opts.renderPage({
|
|
1358
|
-
kind: "success",
|
|
1359
|
-
provider: opts.providerName,
|
|
1360
|
-
message: opts.successMessage
|
|
1361
|
-
}));
|
|
1362
|
-
pending.settle({
|
|
1363
|
-
code,
|
|
1364
|
-
state
|
|
1365
|
-
});
|
|
1366
|
-
} catch {
|
|
1367
|
-
respondError(res, 500, opts, "Internal error while processing the callback.");
|
|
1368
|
-
}
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
function respondError(res, status, opts, message, details) {
|
|
1372
|
-
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
1373
|
-
res.end(opts.renderPage({
|
|
1374
|
-
kind: "error",
|
|
1375
|
-
provider: opts.providerName,
|
|
1376
|
-
message,
|
|
1377
|
-
details
|
|
1378
|
-
}));
|
|
1379
|
-
}
|
|
1380
|
-
function buildStubHandle(redirectUri, server) {
|
|
1381
|
-
return {
|
|
1382
|
-
redirectUri,
|
|
1383
|
-
waitForCode: async () => null,
|
|
1384
|
-
cancelWait: () => {},
|
|
1385
|
-
close: () => {
|
|
1386
|
-
try {
|
|
1387
|
-
server?.close();
|
|
1388
|
-
} catch {}
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Start the loopback HTTP callback server. The returned handle is the
|
|
1394
|
-
* caller's contract — they `await waitForCode()`, race it against manual
|
|
1395
|
-
* paste, then `close()` in a `finally`.
|
|
1396
|
-
*/
|
|
1397
|
-
async function startCallbackServer(opts) {
|
|
1398
|
-
const onListenError = opts.onListenError ?? "reject";
|
|
1399
|
-
const redirectUri = `http://localhost:${opts.port}${opts.path}`;
|
|
1400
|
-
return new Promise((resolve, reject) => {
|
|
1401
|
-
let settled = false;
|
|
1402
|
-
const pending = { settle: () => {} };
|
|
1403
|
-
const waitPromise = new Promise((resolveWait) => {
|
|
1404
|
-
pending.settle = (value) => {
|
|
1405
|
-
if (settled) return;
|
|
1406
|
-
settled = true;
|
|
1407
|
-
resolveWait(value);
|
|
1408
|
-
};
|
|
1409
|
-
});
|
|
1410
|
-
const server = createServer(buildRequestHandler(opts, pending));
|
|
1411
|
-
server.on("error", (err) => {
|
|
1412
|
-
if (onListenError === "resolveWithStub") {
|
|
1413
|
-
pending.settle(null);
|
|
1414
|
-
resolve(buildStubHandle(redirectUri, server));
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
reject(err);
|
|
1418
|
-
});
|
|
1419
|
-
server.listen(opts.port, CALLBACK_HOST, () => {
|
|
1420
|
-
resolve({
|
|
1421
|
-
redirectUri,
|
|
1422
|
-
waitForCode: () => waitPromise,
|
|
1423
|
-
cancelWait: () => pending.settle(null),
|
|
1424
|
-
close: () => {
|
|
1425
|
-
try {
|
|
1426
|
-
server.close();
|
|
1427
|
-
} catch {}
|
|
1428
|
-
}
|
|
1429
|
-
});
|
|
1430
|
-
});
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
//#endregion
|
|
1434
|
-
//#region src/chat/oauth-page/anthropic.ts
|
|
1435
|
-
const CLIENT_ID$1 = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
1436
|
-
const AUTHORIZE_URL$1 = "https://claude.ai/oauth/authorize";
|
|
1437
|
-
const TOKEN_URL$1 = "https://platform.claude.com/v1/oauth/token";
|
|
1438
|
-
const CALLBACK_PORT$1 = 53692;
|
|
1439
|
-
const CALLBACK_PATH$1 = "/callback";
|
|
1440
|
-
const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
1441
|
-
const PROVIDER_NAME$1 = "Anthropic";
|
|
1442
|
-
/**
|
|
1443
|
-
* Parse what the user pasted into the manual-code prompt. Accepts:
|
|
1444
|
-
* - the bare authorization code
|
|
1445
|
-
* - the full `redirect_uri?code=...&state=...` URL
|
|
1446
|
-
* - the `code#state` shorthand Anthropic surfaces on the page
|
|
1447
|
-
* - a raw query string with `code=...&state=...`
|
|
1448
|
-
*
|
|
1449
|
-
* Matches pi-ai's parse table so an existing user's muscle memory still works.
|
|
1450
|
-
*/
|
|
1451
|
-
function parseAuthorizationInput$1(input) {
|
|
1452
|
-
const value = input.trim();
|
|
1453
|
-
if (!value) return {};
|
|
1454
|
-
try {
|
|
1455
|
-
const url = new URL(value);
|
|
1456
|
-
return {
|
|
1457
|
-
code: url.searchParams.get("code") ?? void 0,
|
|
1458
|
-
state: url.searchParams.get("state") ?? void 0
|
|
1459
|
-
};
|
|
1460
|
-
} catch {}
|
|
1461
|
-
if (value.includes("#")) {
|
|
1462
|
-
const [code, state] = value.split("#", 2);
|
|
1463
|
-
return {
|
|
1464
|
-
code,
|
|
1465
|
-
state
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
if (value.includes("code=")) {
|
|
1469
|
-
const params = new URLSearchParams(value);
|
|
1470
|
-
return {
|
|
1471
|
-
code: params.get("code") ?? void 0,
|
|
1472
|
-
state: params.get("state") ?? void 0
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
return { code: value };
|
|
1476
|
-
}
|
|
1477
|
-
async function postJson(url, body) {
|
|
1478
|
-
const response = await fetch(url, {
|
|
1479
|
-
method: "POST",
|
|
1480
|
-
headers: {
|
|
1481
|
-
"Content-Type": "application/json",
|
|
1482
|
-
"Accept": "application/json"
|
|
1483
|
-
},
|
|
1484
|
-
body: JSON.stringify(body),
|
|
1485
|
-
signal: AbortSignal.timeout(3e4)
|
|
1486
|
-
});
|
|
1487
|
-
const responseBody = await response.text();
|
|
1488
|
-
if (!response.ok) throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
|
|
1489
|
-
return responseBody;
|
|
1490
|
-
}
|
|
1491
|
-
async function exchangeAuthorizationCode$1(code, state, verifier, redirectUri) {
|
|
1492
|
-
const responseBody = await postJson(TOKEN_URL$1, {
|
|
1493
|
-
grant_type: "authorization_code",
|
|
1494
|
-
client_id: CLIENT_ID$1,
|
|
1495
|
-
code,
|
|
1496
|
-
state,
|
|
1497
|
-
redirect_uri: redirectUri,
|
|
1498
|
-
code_verifier: verifier
|
|
1499
|
-
});
|
|
1500
|
-
const tokenData = JSON.parse(responseBody);
|
|
1501
|
-
return {
|
|
1502
|
-
refresh: tokenData.refresh_token,
|
|
1503
|
-
access: tokenData.access_token,
|
|
1504
|
-
expires: Date.now() + tokenData.expires_in * 1e3 - 300 * 1e3
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
async function loginAnthropicWithCustomPage(options) {
|
|
1508
|
-
const { verifier, challenge } = await generatePkce();
|
|
1509
|
-
const server = await startCallbackServer({
|
|
1510
|
-
port: CALLBACK_PORT$1,
|
|
1511
|
-
path: CALLBACK_PATH$1,
|
|
1512
|
-
expectedState: verifier,
|
|
1513
|
-
providerName: PROVIDER_NAME$1,
|
|
1514
|
-
renderPage: options.renderPage,
|
|
1515
|
-
successMessage: "Anthropic authentication completed. You can close this window.",
|
|
1516
|
-
onListenError: "reject"
|
|
1517
|
-
});
|
|
1518
|
-
let code;
|
|
1519
|
-
let state;
|
|
1520
|
-
try {
|
|
1521
|
-
const authParams = new URLSearchParams({
|
|
1522
|
-
code: "true",
|
|
1523
|
-
client_id: CLIENT_ID$1,
|
|
1524
|
-
response_type: "code",
|
|
1525
|
-
redirect_uri: server.redirectUri,
|
|
1526
|
-
scope: SCOPES,
|
|
1527
|
-
code_challenge: challenge,
|
|
1528
|
-
code_challenge_method: "S256",
|
|
1529
|
-
state: verifier
|
|
1530
|
-
});
|
|
1531
|
-
options.onAuth({
|
|
1532
|
-
url: `${AUTHORIZE_URL$1}?${authParams.toString()}`,
|
|
1533
|
-
instructions: "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here."
|
|
1534
|
-
});
|
|
1535
|
-
if (options.onManualCodeInput) {
|
|
1536
|
-
let manualInput;
|
|
1537
|
-
let manualError;
|
|
1538
|
-
const manualPromise = options.onManualCodeInput().then((input) => {
|
|
1539
|
-
manualInput = input;
|
|
1540
|
-
server.cancelWait();
|
|
1541
|
-
}).catch((err) => {
|
|
1542
|
-
manualError = err instanceof Error ? err : new Error(String(err));
|
|
1543
|
-
server.cancelWait();
|
|
1544
|
-
});
|
|
1545
|
-
const result = await server.waitForCode();
|
|
1546
|
-
if (manualError) throw manualError;
|
|
1547
|
-
if (result?.code) {
|
|
1548
|
-
code = result.code;
|
|
1549
|
-
state = result.state;
|
|
1550
|
-
} else if (manualInput) {
|
|
1551
|
-
const parsed = parseAuthorizationInput$1(manualInput);
|
|
1552
|
-
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
|
1553
|
-
code = parsed.code;
|
|
1554
|
-
state = parsed.state ?? verifier;
|
|
1555
|
-
}
|
|
1556
|
-
if (!code) {
|
|
1557
|
-
await manualPromise;
|
|
1558
|
-
if (manualError) throw manualError;
|
|
1559
|
-
if (manualInput) {
|
|
1560
|
-
const parsed = parseAuthorizationInput$1(manualInput);
|
|
1561
|
-
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
|
1562
|
-
code = parsed.code;
|
|
1563
|
-
state = parsed.state ?? verifier;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
} else {
|
|
1567
|
-
const result = await server.waitForCode();
|
|
1568
|
-
if (result?.code) {
|
|
1569
|
-
code = result.code;
|
|
1570
|
-
state = result.state;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
if (!code) {
|
|
1574
|
-
const parsed = parseAuthorizationInput$1(await options.onPrompt({
|
|
1575
|
-
message: "Paste the authorization code or full redirect URL:",
|
|
1576
|
-
placeholder: server.redirectUri
|
|
1577
|
-
}));
|
|
1578
|
-
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
|
1579
|
-
code = parsed.code;
|
|
1580
|
-
state = parsed.state ?? verifier;
|
|
1581
|
-
}
|
|
1582
|
-
if (!code) throw new Error("Missing authorization code");
|
|
1583
|
-
if (!state) throw new Error("Missing OAuth state");
|
|
1584
|
-
options.onProgress?.("Exchanging authorization code for tokens...");
|
|
1585
|
-
return await exchangeAuthorizationCode$1(code, state, verifier, server.redirectUri);
|
|
1586
|
-
} finally {
|
|
1587
|
-
server.close();
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Build an `OAuthProviderInterface` that behaves identically to pi-ai's
|
|
1592
|
-
* `anthropicOAuthProvider` except for the callback page HTML. Drop this
|
|
1593
|
-
* onto `ProviderDescriptor.oauthProvider` to override.
|
|
1594
|
-
*/
|
|
1595
|
-
function createAnthropicOAuthProviderWithCustomPage(renderPage) {
|
|
1596
|
-
return {
|
|
1597
|
-
id: "anthropic",
|
|
1598
|
-
name: "Anthropic (Claude Pro/Max)",
|
|
1599
|
-
usesCallbackServer: true,
|
|
1600
|
-
async login(callbacks) {
|
|
1601
|
-
return loginAnthropicWithCustomPage({
|
|
1602
|
-
renderPage,
|
|
1603
|
-
onAuth: callbacks.onAuth,
|
|
1604
|
-
onPrompt: callbacks.onPrompt,
|
|
1605
|
-
onProgress: callbacks.onProgress,
|
|
1606
|
-
onManualCodeInput: callbacks.onManualCodeInput
|
|
1607
|
-
});
|
|
1608
|
-
},
|
|
1609
|
-
async refreshToken(credentials) {
|
|
1610
|
-
return refreshAnthropicToken(credentials.refresh);
|
|
1611
|
-
},
|
|
1612
|
-
getApiKey(credentials) {
|
|
1613
|
-
return credentials.access;
|
|
1614
|
-
}
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
//#endregion
|
|
1618
|
-
//#region src/chat/oauth-page/openai-codex.ts
|
|
1619
|
-
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
1620
|
-
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
1621
|
-
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
1622
|
-
const CALLBACK_PORT = 1455;
|
|
1623
|
-
const CALLBACK_PATH = "/auth/callback";
|
|
1624
|
-
const SCOPE = "openid profile email offline_access";
|
|
1625
|
-
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
1626
|
-
const PROVIDER_NAME = "OpenAI Codex";
|
|
1627
|
-
function createState() {
|
|
1628
|
-
return randomBytes(16).toString("hex");
|
|
1629
|
-
}
|
|
1630
|
-
function parseAuthorizationInput(input) {
|
|
1631
|
-
const value = input.trim();
|
|
1632
|
-
if (!value) return {};
|
|
1633
|
-
try {
|
|
1634
|
-
const url = new URL(value);
|
|
1635
|
-
return {
|
|
1636
|
-
code: url.searchParams.get("code") ?? void 0,
|
|
1637
|
-
state: url.searchParams.get("state") ?? void 0
|
|
1638
|
-
};
|
|
1639
|
-
} catch {}
|
|
1640
|
-
if (value.includes("#")) {
|
|
1641
|
-
const [code, state] = value.split("#", 2);
|
|
1642
|
-
return {
|
|
1643
|
-
code,
|
|
1644
|
-
state
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
if (value.includes("code=")) {
|
|
1648
|
-
const params = new URLSearchParams(value);
|
|
1649
|
-
return {
|
|
1650
|
-
code: params.get("code") ?? void 0,
|
|
1651
|
-
state: params.get("state") ?? void 0
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
return { code: value };
|
|
1655
|
-
}
|
|
1656
|
-
function decodeJwt(token) {
|
|
1657
|
-
try {
|
|
1658
|
-
const parts = token.split(".");
|
|
1659
|
-
if (parts.length !== 3) return null;
|
|
1660
|
-
const payload = parts[1] ?? "";
|
|
1661
|
-
const decoded = atob(payload);
|
|
1662
|
-
return JSON.parse(decoded);
|
|
1663
|
-
} catch {
|
|
1664
|
-
return null;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
function getAccountId(accessToken) {
|
|
1668
|
-
const accountId = (decodeJwt(accessToken)?.[JWT_CLAIM_PATH])?.chatgpt_account_id;
|
|
1669
|
-
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
1670
|
-
}
|
|
1671
|
-
async function exchangeAuthorizationCode(code, verifier, redirectUri) {
|
|
1672
|
-
const response = await fetch(TOKEN_URL, {
|
|
1673
|
-
method: "POST",
|
|
1674
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1675
|
-
body: new URLSearchParams({
|
|
1676
|
-
grant_type: "authorization_code",
|
|
1677
|
-
client_id: CLIENT_ID,
|
|
1678
|
-
code,
|
|
1679
|
-
code_verifier: verifier,
|
|
1680
|
-
redirect_uri: redirectUri
|
|
1681
|
-
})
|
|
1682
|
-
});
|
|
1683
|
-
if (!response.ok) {
|
|
1684
|
-
const text = await response.text().catch(() => "");
|
|
1685
|
-
return {
|
|
1686
|
-
type: "failed",
|
|
1687
|
-
message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
const json = await response.json();
|
|
1691
|
-
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") return {
|
|
1692
|
-
type: "failed",
|
|
1693
|
-
message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`
|
|
1694
|
-
};
|
|
1695
|
-
return {
|
|
1696
|
-
type: "success",
|
|
1697
|
-
access: json.access_token,
|
|
1698
|
-
refresh: json.refresh_token,
|
|
1699
|
-
expires: Date.now() + json.expires_in * 1e3
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
async function loginOpenAICodexWithCustomPage(options) {
|
|
1703
|
-
const { verifier, challenge } = await generatePkce();
|
|
1704
|
-
const state = createState();
|
|
1705
|
-
const originator = options.originator ?? "pi";
|
|
1706
|
-
const server = await startCallbackServer({
|
|
1707
|
-
port: CALLBACK_PORT,
|
|
1708
|
-
path: CALLBACK_PATH,
|
|
1709
|
-
expectedState: state,
|
|
1710
|
-
providerName: PROVIDER_NAME,
|
|
1711
|
-
renderPage: options.renderPage,
|
|
1712
|
-
successMessage: "OpenAI authentication completed. You can close this window.",
|
|
1713
|
-
onListenError: "resolveWithStub"
|
|
1714
|
-
});
|
|
1715
|
-
const authUrl = new URL(AUTHORIZE_URL);
|
|
1716
|
-
authUrl.searchParams.set("response_type", "code");
|
|
1717
|
-
authUrl.searchParams.set("client_id", CLIENT_ID);
|
|
1718
|
-
authUrl.searchParams.set("redirect_uri", server.redirectUri);
|
|
1719
|
-
authUrl.searchParams.set("scope", SCOPE);
|
|
1720
|
-
authUrl.searchParams.set("code_challenge", challenge);
|
|
1721
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1722
|
-
authUrl.searchParams.set("state", state);
|
|
1723
|
-
authUrl.searchParams.set("id_token_add_organizations", "true");
|
|
1724
|
-
authUrl.searchParams.set("codex_cli_simplified_flow", "true");
|
|
1725
|
-
authUrl.searchParams.set("originator", originator);
|
|
1726
|
-
options.onAuth({
|
|
1727
|
-
url: authUrl.toString(),
|
|
1728
|
-
instructions: "A browser window should open. Complete login to finish."
|
|
1729
|
-
});
|
|
1730
|
-
let code;
|
|
1731
|
-
try {
|
|
1732
|
-
if (options.onManualCodeInput) {
|
|
1733
|
-
let manualCode;
|
|
1734
|
-
let manualError;
|
|
1735
|
-
const manualPromise = options.onManualCodeInput().then((input) => {
|
|
1736
|
-
manualCode = input;
|
|
1737
|
-
server.cancelWait();
|
|
1738
|
-
}).catch((err) => {
|
|
1739
|
-
manualError = err instanceof Error ? err : new Error(String(err));
|
|
1740
|
-
server.cancelWait();
|
|
1741
|
-
});
|
|
1742
|
-
const result = await server.waitForCode();
|
|
1743
|
-
if (manualError) throw manualError;
|
|
1744
|
-
if (result?.code) code = result.code;
|
|
1745
|
-
else if (manualCode) {
|
|
1746
|
-
const parsed = parseAuthorizationInput(manualCode);
|
|
1747
|
-
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
1748
|
-
code = parsed.code;
|
|
1749
|
-
}
|
|
1750
|
-
if (!code) {
|
|
1751
|
-
await manualPromise;
|
|
1752
|
-
if (manualError) throw manualError;
|
|
1753
|
-
if (manualCode) {
|
|
1754
|
-
const parsed = parseAuthorizationInput(manualCode);
|
|
1755
|
-
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
1756
|
-
code = parsed.code;
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
} else {
|
|
1760
|
-
const result = await server.waitForCode();
|
|
1761
|
-
if (result?.code) code = result.code;
|
|
1762
|
-
}
|
|
1763
|
-
if (!code) {
|
|
1764
|
-
const parsed = parseAuthorizationInput(await options.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }));
|
|
1765
|
-
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
|
1766
|
-
code = parsed.code;
|
|
1767
|
-
}
|
|
1768
|
-
if (!code) throw new Error("Missing authorization code");
|
|
1769
|
-
const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri);
|
|
1770
|
-
if (tokenResult.type !== "success") throw new Error(tokenResult.message);
|
|
1771
|
-
const accountId = getAccountId(tokenResult.access);
|
|
1772
|
-
if (!accountId) throw new Error("Failed to extract accountId from token");
|
|
1773
|
-
return {
|
|
1774
|
-
access: tokenResult.access,
|
|
1775
|
-
refresh: tokenResult.refresh,
|
|
1776
|
-
expires: tokenResult.expires,
|
|
1777
|
-
accountId
|
|
1778
|
-
};
|
|
1779
|
-
} finally {
|
|
1780
|
-
server.close();
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
/**
|
|
1784
|
-
* Build an `OAuthProviderInterface` that behaves identically to pi-ai's
|
|
1785
|
-
* `openaiCodexOAuthProvider` except for the callback page HTML.
|
|
1786
|
-
*/
|
|
1787
|
-
function createOpenAICodexOAuthProviderWithCustomPage(renderPage) {
|
|
1788
|
-
return {
|
|
1789
|
-
id: "openai-codex",
|
|
1790
|
-
name: "ChatGPT Plus/Pro (Codex Subscription)",
|
|
1791
|
-
usesCallbackServer: true,
|
|
1792
|
-
async login(callbacks) {
|
|
1793
|
-
return loginOpenAICodexWithCustomPage({
|
|
1794
|
-
renderPage,
|
|
1795
|
-
onAuth: callbacks.onAuth,
|
|
1796
|
-
onPrompt: callbacks.onPrompt,
|
|
1797
|
-
onProgress: callbacks.onProgress,
|
|
1798
|
-
onManualCodeInput: callbacks.onManualCodeInput
|
|
1799
|
-
});
|
|
1800
|
-
},
|
|
1801
|
-
async refreshToken(credentials) {
|
|
1802
|
-
return refreshOpenAICodexToken(credentials.refresh);
|
|
1803
|
-
},
|
|
1804
|
-
getApiKey(credentials) {
|
|
1805
|
-
return credentials.access;
|
|
1806
|
-
}
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
//#endregion
|
|
1810
|
-
//#region src/chat/oauth-page/render.ts
|
|
1811
|
-
function escapeHtml(value) {
|
|
1812
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Default zidane-themed page. Visually neutral but distinguishable from
|
|
1816
|
-
* pi-ai's stock page — dark background, mono headings, no logo (host can
|
|
1817
|
-
* pass a custom renderer to add one).
|
|
1818
|
-
*/
|
|
1819
|
-
const renderDefaultCallbackPage = (page) => {
|
|
1820
|
-
const heading = escapeHtml(page.kind === "success" ? `Signed in to ${page.provider}` : `Could not sign in to ${page.provider}`);
|
|
1821
|
-
const message = escapeHtml(page.message);
|
|
1822
|
-
const details = page.details ? escapeHtml(page.details) : void 0;
|
|
1823
|
-
return `<!doctype html>
|
|
1824
|
-
<html lang="en">
|
|
1825
|
-
<head>
|
|
1826
|
-
<meta charset="utf-8" />
|
|
1827
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1828
|
-
<title>${heading}</title>
|
|
1829
|
-
<style>
|
|
1830
|
-
:root {
|
|
1831
|
-
--text: #f4f4f5;
|
|
1832
|
-
--text-dim: #a1a1aa;
|
|
1833
|
-
--accent: ${page.kind === "success" ? "#22d3ee" : "#f87171"};
|
|
1834
|
-
--page-bg: #0a0a0a;
|
|
1835
|
-
--panel-bg: #131316;
|
|
1836
|
-
--border: #27272a;
|
|
1837
|
-
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
1838
|
-
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
1839
|
-
}
|
|
1840
|
-
* { box-sizing: border-box; }
|
|
1841
|
-
html { color-scheme: dark; }
|
|
1842
|
-
body {
|
|
1843
|
-
margin: 0;
|
|
1844
|
-
min-height: 100vh;
|
|
1845
|
-
display: flex;
|
|
1846
|
-
align-items: center;
|
|
1847
|
-
justify-content: center;
|
|
1848
|
-
padding: 24px;
|
|
1849
|
-
background: var(--page-bg);
|
|
1850
|
-
color: var(--text);
|
|
1851
|
-
font-family: var(--font-sans);
|
|
1852
|
-
}
|
|
1853
|
-
main {
|
|
1854
|
-
width: 100%;
|
|
1855
|
-
max-width: 520px;
|
|
1856
|
-
padding: 32px;
|
|
1857
|
-
background: var(--panel-bg);
|
|
1858
|
-
border: 1px solid var(--border);
|
|
1859
|
-
border-radius: 12px;
|
|
1860
|
-
}
|
|
1861
|
-
.label {
|
|
1862
|
-
font-family: var(--font-mono);
|
|
1863
|
-
font-size: 12px;
|
|
1864
|
-
letter-spacing: 0.08em;
|
|
1865
|
-
text-transform: uppercase;
|
|
1866
|
-
color: var(--accent);
|
|
1867
|
-
margin-bottom: 12px;
|
|
1868
|
-
}
|
|
1869
|
-
h1 {
|
|
1870
|
-
margin: 0 0 12px;
|
|
1871
|
-
font-size: 22px;
|
|
1872
|
-
font-weight: 600;
|
|
1873
|
-
letter-spacing: -0.01em;
|
|
1874
|
-
color: var(--text);
|
|
1875
|
-
}
|
|
1876
|
-
p {
|
|
1877
|
-
margin: 0;
|
|
1878
|
-
line-height: 1.6;
|
|
1879
|
-
color: var(--text-dim);
|
|
1880
|
-
font-size: 14px;
|
|
1881
|
-
}
|
|
1882
|
-
.details {
|
|
1883
|
-
margin-top: 18px;
|
|
1884
|
-
padding: 12px 14px;
|
|
1885
|
-
background: var(--page-bg);
|
|
1886
|
-
border: 1px solid var(--border);
|
|
1887
|
-
border-radius: 8px;
|
|
1888
|
-
font-family: var(--font-mono);
|
|
1889
|
-
font-size: 12px;
|
|
1890
|
-
color: var(--text-dim);
|
|
1891
|
-
white-space: pre-wrap;
|
|
1892
|
-
word-break: break-word;
|
|
1893
|
-
}
|
|
1894
|
-
</style>
|
|
1895
|
-
</head>
|
|
1896
|
-
<body>
|
|
1897
|
-
<main>
|
|
1898
|
-
<div class="label">zidane · OAuth · ${escapeHtml(page.provider)}</div>
|
|
1899
|
-
<h1>${heading}</h1>
|
|
1900
|
-
<p>${message}</p>
|
|
1901
|
-
${details ? `<div class="details">${details}</div>` : ""}
|
|
1902
|
-
</main>
|
|
1903
|
-
</body>
|
|
1904
|
-
</html>`;
|
|
1905
|
-
};
|
|
1906
|
-
//#endregion
|
|
1907
|
-
//#region src/chat/oauth-page/index.ts
|
|
1908
|
-
/**
|
|
1909
|
-
* Bundle helper — returns both Anthropic + OpenAI Codex providers wired
|
|
1910
|
-
* with the same renderer. Hosts that want different renderers per provider
|
|
1911
|
-
* should call the individual `create…WithCustomPage` factories instead.
|
|
1912
|
-
*/
|
|
1913
|
-
function createCustomCallbackOAuthProviders(renderPage) {
|
|
1914
|
-
return {
|
|
1915
|
-
anthropic: createAnthropicOAuthProviderWithCustomPage(renderPage),
|
|
1916
|
-
openaiCodex: createOpenAICodexOAuthProviderWithCustomPage(renderPage)
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
|
-
//#endregion
|
|
1920
|
-
//#region src/chat/providers.ts
|
|
1921
|
-
/**
|
|
1922
|
-
* pi-ai's stock Anthropic + Codex OAuth providers bake their own callback
|
|
1923
|
-
* HTML; we route both through `renderDefaultCallbackPage` so the post-redirect
|
|
1924
|
-
* page matches zidane's theme. The override lives in `src/chat/oauth-page/`
|
|
1925
|
-
* — see that folder's `index.ts` for the deletion path once pi-ai exposes
|
|
1926
|
-
* a `renderCallbackPage` hook upstream.
|
|
1927
|
-
*/
|
|
1928
|
-
const { anthropic: anthropicOAuthProvider, openaiCodex: openaiCodexOAuthProvider } = createCustomCallbackOAuthProviders(renderDefaultCallbackPage);
|
|
1929
|
-
/** Convenience accessor — returns `credentialFileKey ?? key`. */
|
|
1930
|
-
function credKeyOf(desc) {
|
|
1931
|
-
return desc.credentialFileKey ?? desc.key;
|
|
1932
|
-
}
|
|
1933
|
-
/** Convenience accessor — returns `piProviderId ?? key`. */
|
|
1934
|
-
function piIdOf(desc) {
|
|
1935
|
-
return desc.piProviderId ?? desc.key;
|
|
1936
|
-
}
|
|
1937
|
-
const anthropicDescriptor = {
|
|
1938
|
-
key: "anthropic",
|
|
1939
|
-
label: "Anthropic",
|
|
1940
|
-
factory: anthropic,
|
|
1941
|
-
defaultModel: "claude-opus-4-7",
|
|
1942
|
-
envKey: "ANTHROPIC_API_KEY",
|
|
1943
|
-
apiKeyPlaceholder: "sk-ant-…",
|
|
1944
|
-
oauthProvider: anthropicOAuthProvider,
|
|
1945
|
-
oauthHint: "Claude Pro/Max subscription"
|
|
1946
|
-
};
|
|
1947
|
-
const openaiDescriptor = {
|
|
1948
|
-
key: "openai",
|
|
1949
|
-
label: "OpenAI Codex",
|
|
1950
|
-
factory: openai,
|
|
1951
|
-
defaultModel: "gpt-5.4",
|
|
1952
|
-
envKey: "OPENAI_CODEX_API_KEY",
|
|
1953
|
-
credentialFileKey: "openai-codex",
|
|
1954
|
-
piProviderId: "openai-codex",
|
|
1955
|
-
apiKeyPlaceholder: "sk-… or eyJ… (Codex)",
|
|
1956
|
-
oauthProvider: openaiCodexOAuthProvider
|
|
1957
|
-
};
|
|
1958
|
-
const openrouterDescriptor = {
|
|
1959
|
-
key: "openrouter",
|
|
1960
|
-
label: "OpenRouter",
|
|
1961
|
-
factory: openrouter,
|
|
1962
|
-
defaultModel: "anthropic/claude-sonnet-4-6",
|
|
1963
|
-
envKey: "OPENROUTER_API_KEY",
|
|
1964
|
-
apiKeyPlaceholder: "sk-or-…"
|
|
1965
|
-
};
|
|
1966
|
-
const cerebrasDescriptor = {
|
|
1967
|
-
key: "cerebras",
|
|
1968
|
-
label: "Cerebras",
|
|
1969
|
-
factory: cerebras,
|
|
1970
|
-
defaultModel: "zai-glm-4.7",
|
|
1971
|
-
envKey: "CEREBRAS_API_KEY",
|
|
1972
|
-
apiKeyPlaceholder: "csk-…"
|
|
1973
|
-
};
|
|
1974
|
-
/**
|
|
1975
|
-
* Default provider registry. Passed verbatim when `runTui` is invoked without
|
|
1976
|
-
* an explicit `providers` option. Hosts that want to override per-provider
|
|
1977
|
-
* metadata can spread this and replace specific entries:
|
|
1978
|
-
*
|
|
1979
|
-
* ```ts
|
|
1980
|
-
* runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })
|
|
1981
|
-
* ```
|
|
1982
|
-
*/
|
|
1983
|
-
const BUILTIN_PROVIDERS = {
|
|
1984
|
-
anthropic: anthropicDescriptor,
|
|
1985
|
-
openai: openaiDescriptor,
|
|
1986
|
-
openrouter: openrouterDescriptor,
|
|
1987
|
-
cerebras: cerebrasDescriptor
|
|
1988
|
-
};
|
|
1989
|
-
/**
|
|
1990
|
-
* Resolve the model list for a given provider. Honors `descriptor.models`
|
|
1991
|
-
* when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns
|
|
1992
|
-
* `[]` for descriptors with no known mapping (custom providers without a
|
|
1993
|
-
* model list) — callers should hide the model picker in that case.
|
|
1994
|
-
*/
|
|
1995
|
-
function modelsForDescriptor(descriptor) {
|
|
1996
|
-
if (descriptor.models) return descriptor.models;
|
|
1997
|
-
try {
|
|
1998
|
-
return getModels(piIdOf(descriptor));
|
|
1999
|
-
} catch {
|
|
2000
|
-
return [];
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* Resolve a single model's metadata via the descriptor's model source.
|
|
2005
|
-
* Mirrors {@link modelsForDescriptor} routing: descriptor's own list wins,
|
|
2006
|
-
* pi-ai's registry is the fallback. Returns `null` when the model isn't
|
|
2007
|
-
* known.
|
|
2008
|
-
*/
|
|
2009
|
-
function getModelInfo(descriptor, modelId) {
|
|
2010
|
-
if (descriptor.models) return descriptor.models.find((m) => m.id === modelId) ?? null;
|
|
2011
|
-
try {
|
|
2012
|
-
return getModel(piIdOf(descriptor), modelId) ?? null;
|
|
2013
|
-
} catch {
|
|
2014
|
-
return null;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
/**
|
|
2018
|
-
* Look up the model's max context window via the descriptor's model source.
|
|
2019
|
-
* Returns `null` when the model isn't known (custom slugs, providers without
|
|
2020
|
-
* a registry); callers should hide the context indicator in that case.
|
|
2021
|
-
*/
|
|
2022
|
-
function getContextWindow(descriptor, modelId) {
|
|
2023
|
-
return getModelInfo(descriptor, modelId)?.contextWindow ?? null;
|
|
2024
|
-
}
|
|
2025
|
-
/**
|
|
2026
|
-
* Reserved output budget subtracted from the raw context window when
|
|
2027
|
-
* computing the effective ceiling for compaction / warning thresholds.
|
|
2028
|
-
*
|
|
2029
|
-
* Aligned with Claude Code's `COMPACT_MAX_OUTPUT_TOKENS = 20_000`
|
|
2030
|
-
* (`utils/context.ts:12`) — covers p99.99 of summary + response output.
|
|
2031
|
-
* A turn that consumed N input tokens leaves `effectiveWindow - N`
|
|
2032
|
-
* headroom for the next prompt's response; once headroom shrinks
|
|
2033
|
-
* below the threshold, auto-compaction fires.
|
|
2034
|
-
*/
|
|
2035
|
-
const OUTPUT_RESERVE_TOKENS = 2e4;
|
|
2036
|
-
/**
|
|
2037
|
-
* Effective context window — what the next user turn can actually pack
|
|
2038
|
-
* before the provider reserves space for the assistant's reply. Equals
|
|
2039
|
-
* `rawWindow - OUTPUT_RESERVE_TOKENS`, clamped to `>= 1` so a tiny
|
|
2040
|
-
* (or absurd) window doesn't yield zero / negative thresholds.
|
|
2041
|
-
*
|
|
2042
|
-
* Used by both the footer's context indicator and the auto-compact
|
|
2043
|
-
* trigger so the two surfaces agree on "how full are we, really".
|
|
2044
|
-
* Pass `null` through unchanged so callers can pipe `getContextWindow`
|
|
2045
|
-
* directly without an intermediate check.
|
|
2046
|
-
*/
|
|
2047
|
-
function effectiveContextWindow(rawWindow) {
|
|
2048
|
-
if (rawWindow === null) return null;
|
|
2049
|
-
return Math.max(1, rawWindow - OUTPUT_RESERVE_TOKENS);
|
|
2050
|
-
}
|
|
2051
|
-
/**
|
|
2052
|
-
* Whether the given model exposes a reasoning / extended-thinking knob.
|
|
2053
|
-
* Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and
|
|
2054
|
-
* the bottom-bar `effortName` segment only surface when this is `true`.
|
|
2055
|
-
* Returns `false` for unknown models (no registry entry → no advertised
|
|
2056
|
-
* capability).
|
|
2057
|
-
*/
|
|
2058
|
-
function modelSupportsReasoning(descriptor, modelId) {
|
|
2059
|
-
return getModelInfo(descriptor, modelId)?.reasoning === true;
|
|
2060
|
-
}
|
|
2061
|
-
//#endregion
|
|
2062
1309
|
//#region src/chat/credentials.ts
|
|
2063
1310
|
/** POSIX mode for the credentials file. Ignored on Windows. */
|
|
2064
1311
|
const FILE_MODE$1 = 384;
|
|
@@ -2159,10 +1406,21 @@ function removeProviderCredential(dataDir, descriptor) {
|
|
|
2159
1406
|
function applyApiKeyEnv(dataDir, registry) {
|
|
2160
1407
|
const creds = readCredentials(dataDir);
|
|
2161
1408
|
for (const descriptor of Object.values(registry)) {
|
|
1409
|
+
const cred = creds[credKeyOf(descriptor)];
|
|
1410
|
+
if (cred?.kind === "apikey" && descriptor.customFields) {
|
|
1411
|
+
const stored = cred.customFields ?? {};
|
|
1412
|
+
for (const field of descriptor.customFields) {
|
|
1413
|
+
const value = stored[field.key];
|
|
1414
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1415
|
+
const ambient = process.env[field.envVar];
|
|
1416
|
+
if (ambient && ambient !== value && process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] applyApiKeyEnv: overriding ambient \`${field.envVar}\` with stored value from credentials.json (provider=${descriptor.key}, field=${field.key}).\n`);
|
|
1417
|
+
process.env[field.envVar] = value;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
2162
1421
|
if (!descriptor.envKey) continue;
|
|
2163
1422
|
const envKey = descriptor.envKey;
|
|
2164
1423
|
const ambient = process.env[envKey];
|
|
2165
|
-
const cred = creds[credKeyOf(descriptor)];
|
|
2166
1424
|
if (cred?.kind === "apikey" && cred.value) {
|
|
2167
1425
|
if (ambient && ambient !== cred.value && process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] applyApiKeyEnv: overriding ambient \`${envKey}\` with stored API key from credentials.json (provider=${descriptor.key}). Sign out via the auth wizard if the ambient value was intended to win.\n`);
|
|
2168
1426
|
process.env[envKey] = cred.value;
|
|
@@ -2251,6 +1509,13 @@ function detectAuth(dataDir, registry, env = process.env) {
|
|
|
2251
1509
|
source: "apikey",
|
|
2252
1510
|
detail: "credentials.json"
|
|
2253
1511
|
});
|
|
1512
|
+
if (fileEntry?.kind === "apikey" && !fileEntry.value && descriptor.customFields && descriptor.customFields.length > 0) {
|
|
1513
|
+
const stored = fileEntry.customFields ?? {};
|
|
1514
|
+
if (descriptor.customFields.filter((f) => f.required).every((f) => typeof stored[f.key] === "string" && stored[f.key].length > 0)) methods.push({
|
|
1515
|
+
source: "apikey",
|
|
1516
|
+
detail: "credentials.json (configured)"
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
2254
1519
|
if (descriptor.envKey && env[descriptor.envKey]) methods.push({
|
|
2255
1520
|
source: "env",
|
|
2256
1521
|
detail: descriptor.envKey
|
|
@@ -3235,192 +2500,8 @@ function tryOpenBrowser(url) {
|
|
|
3235
2500
|
} catch {}
|
|
3236
2501
|
}
|
|
3237
2502
|
//#endregion
|
|
3238
|
-
//#region src/chat/color-gradient.ts
|
|
3239
|
-
/** Parse `#rrggbb` (case-insensitive) into `[r, g, b]` 0–255 integers. */
|
|
3240
|
-
function parseHex(hex) {
|
|
3241
|
-
const h = hex.replace("#", "");
|
|
3242
|
-
return [
|
|
3243
|
-
Number.parseInt(h.slice(0, 2), 16),
|
|
3244
|
-
Number.parseInt(h.slice(2, 4), 16),
|
|
3245
|
-
Number.parseInt(h.slice(4, 6), 16)
|
|
3246
|
-
];
|
|
3247
|
-
}
|
|
3248
|
-
/** Convert sRGB 0–255 → HSL 0–1. */
|
|
3249
|
-
function rgbToHsl(r, g, b) {
|
|
3250
|
-
r /= 255;
|
|
3251
|
-
g /= 255;
|
|
3252
|
-
b /= 255;
|
|
3253
|
-
const max = Math.max(r, g, b);
|
|
3254
|
-
const min = Math.min(r, g, b);
|
|
3255
|
-
const l = (max + min) / 2;
|
|
3256
|
-
if (max === min) return [
|
|
3257
|
-
0,
|
|
3258
|
-
0,
|
|
3259
|
-
l
|
|
3260
|
-
];
|
|
3261
|
-
const d = max - min;
|
|
3262
|
-
const s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
3263
|
-
let h;
|
|
3264
|
-
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
3265
|
-
else if (max === g) h = (b - r) / d + 2;
|
|
3266
|
-
else h = (r - g) / d + 4;
|
|
3267
|
-
return [
|
|
3268
|
-
h / 6,
|
|
3269
|
-
s,
|
|
3270
|
-
l
|
|
3271
|
-
];
|
|
3272
|
-
}
|
|
3273
|
-
/** Convert HSL 0–1 → sRGB 0–255. Standard piecewise formula. */
|
|
3274
|
-
function hslToRgb(h, s, l) {
|
|
3275
|
-
if (s === 0) return [
|
|
3276
|
-
l * 255,
|
|
3277
|
-
l * 255,
|
|
3278
|
-
l * 255
|
|
3279
|
-
];
|
|
3280
|
-
const hue2rgb = (p, q, t) => {
|
|
3281
|
-
if (t < 0) t += 1;
|
|
3282
|
-
if (t > 1) t -= 1;
|
|
3283
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
3284
|
-
if (t < 1 / 2) return q;
|
|
3285
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
3286
|
-
return p;
|
|
3287
|
-
};
|
|
3288
|
-
const q = l < .5 ? l * (1 + s) : l + s - l * s;
|
|
3289
|
-
const p = 2 * l - q;
|
|
3290
|
-
return [
|
|
3291
|
-
hue2rgb(p, q, h + 1 / 3) * 255,
|
|
3292
|
-
hue2rgb(p, q, h) * 255,
|
|
3293
|
-
hue2rgb(p, q, h - 1 / 3) * 255
|
|
3294
|
-
];
|
|
3295
|
-
}
|
|
3296
|
-
function toHex(rgb) {
|
|
3297
|
-
const pad = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
3298
|
-
return `#${pad(rgb[0])}${pad(rgb[1])}${pad(rgb[2])}`;
|
|
3299
|
-
}
|
|
3300
|
-
/**
|
|
3301
|
-
* Blend two hex colors in HSL space with shortest-path hue interpolation.
|
|
3302
|
-
* `t` ∈ [0, 1]; `t=0` returns `from`, `t=1` returns `to`.
|
|
3303
|
-
*/
|
|
3304
|
-
function blendHsl(from, to, t) {
|
|
3305
|
-
const [r1, g1, b1] = parseHex(from);
|
|
3306
|
-
const [r2, g2, b2] = parseHex(to);
|
|
3307
|
-
const [h1, s1, l1] = rgbToHsl(r1, g1, b1);
|
|
3308
|
-
const [h2, s2, l2] = rgbToHsl(r2, g2, b2);
|
|
3309
|
-
let dh = h2 - h1;
|
|
3310
|
-
if (dh > .5) dh -= 1;
|
|
3311
|
-
else if (dh < -.5) dh += 1;
|
|
3312
|
-
return toHex(hslToRgb((h1 + dh * t + 1) % 1, s1 + (s2 - s1) * t, l1 + (l2 - l1) * t));
|
|
3313
|
-
}
|
|
3314
|
-
/**
|
|
3315
|
-
* Static gradient ramp of length `n` going from `from` (index 0) to
|
|
3316
|
-
* `to` (index n-1) in HSL space. For the cycling A→B→A→B ramp the
|
|
3317
|
-
* throbber uses, see `buildCycleRamp` in `src/tui/crush-throbber.tsx`.
|
|
3318
|
-
*/
|
|
3319
|
-
function buildLinearRamp(from, to, n) {
|
|
3320
|
-
if (n <= 0) return [];
|
|
3321
|
-
if (n === 1) return [blendHsl(from, to, .5)];
|
|
3322
|
-
const ramp = [];
|
|
3323
|
-
for (let i = 0; i < n; i++) ramp.push(blendHsl(from, to, i / (n - 1)));
|
|
3324
|
-
return ramp;
|
|
3325
|
-
}
|
|
3326
|
-
//#endregion
|
|
3327
2503
|
//#region src/chat/completion.ts
|
|
3328
2504
|
/**
|
|
3329
|
-
* Prompt autocompletion framework.
|
|
3330
|
-
*
|
|
3331
|
-
* Renderer-agnostic. Providers plug in by registering a `trigger` character
|
|
3332
|
-
* (e.g. `/` for skills, `@` for files) and exposing two operations:
|
|
3333
|
-
*
|
|
3334
|
-
* 1. `suggest(query)` — return ranked items for the live query.
|
|
3335
|
-
* 2. `parseReferences(text)` — find all references to the provider's
|
|
3336
|
-
* items in arbitrary text. Used to highlight in-prompt mentions and
|
|
3337
|
-
* drive submit-time side effects (activate the skill, attach the
|
|
3338
|
-
* file, …).
|
|
3339
|
-
*
|
|
3340
|
-
* The TUI consumes `useCompletion()` to drive a popover above the textarea.
|
|
3341
|
-
* A future GUI consumes the same hook to drive a dropdown. The popup
|
|
3342
|
-
* component itself only reads `label` + `description` on each item, so
|
|
3343
|
-
* provider-specific typing (`TItem`) stays at the registration boundary
|
|
3344
|
-
* and never leaks into the renderer.
|
|
3345
|
-
*/
|
|
3346
|
-
/**
|
|
3347
|
-
* Resolve the provider trigger active at `cursor`, or `null` when none fits.
|
|
3348
|
-
*
|
|
3349
|
-
* Rules:
|
|
3350
|
-
* - The trigger character must sit at position 0 of the buffer OR be
|
|
3351
|
-
* preceded by whitespace. This prevents `http://` from triggering the
|
|
3352
|
-
* `/`-bound skills provider mid-URL.
|
|
3353
|
-
* - The cursor must be at or past the trigger position.
|
|
3354
|
-
* - Nothing between the trigger and the cursor may be whitespace (the
|
|
3355
|
-
* query is one contiguous token).
|
|
3356
|
-
* - The query length is bounded — `maxQueryLength` defaults to 64 — so
|
|
3357
|
-
* a runaway buffer scan can't pin the renderer.
|
|
3358
|
-
*/
|
|
3359
|
-
function findActiveTrigger(text, cursor, providers, options = {}) {
|
|
3360
|
-
if (providers.length === 0) return null;
|
|
3361
|
-
const max = options.maxQueryLength ?? 64;
|
|
3362
|
-
const safeCursor = Math.max(0, Math.min(cursor, text.length));
|
|
3363
|
-
const isWhitespace = (ch) => ch === void 0 ? false : /\s/.test(ch);
|
|
3364
|
-
for (let i = safeCursor - 1; i >= 0 && safeCursor - i <= max + 1; i--) {
|
|
3365
|
-
const ch = text[i];
|
|
3366
|
-
if (isWhitespace(ch)) return null;
|
|
3367
|
-
const provider = providers.find((p) => p.trigger === ch);
|
|
3368
|
-
if (!provider) continue;
|
|
3369
|
-
const before = i > 0 ? text[i - 1] : "";
|
|
3370
|
-
if (before !== "" && !isWhitespace(before)) continue;
|
|
3371
|
-
return {
|
|
3372
|
-
provider,
|
|
3373
|
-
query: text.slice(i + 1, safeCursor),
|
|
3374
|
-
span: {
|
|
3375
|
-
start: i,
|
|
3376
|
-
end: safeCursor
|
|
3377
|
-
}
|
|
3378
|
-
};
|
|
3379
|
-
}
|
|
3380
|
-
return null;
|
|
3381
|
-
}
|
|
3382
|
-
/**
|
|
3383
|
-
* Replace `[span.start, span.end)` in `text` with `insertText`. Returns the
|
|
3384
|
-
* mutated text and the new cursor position (end of insertion).
|
|
3385
|
-
*/
|
|
3386
|
-
function applyInsert(text, span, insertText) {
|
|
3387
|
-
return {
|
|
3388
|
-
text: text.slice(0, span.start) + insertText + text.slice(span.end),
|
|
3389
|
-
cursor: span.start + insertText.length
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3392
|
-
/**
|
|
3393
|
-
* Merge reference lists from multiple providers into one ordered list with
|
|
3394
|
-
* earlier-start-wins disambiguation when spans overlap. Ties broken by
|
|
3395
|
-
* insertion order. Spans are sorted ascending so renderers can walk them
|
|
3396
|
-
* sequentially with a cursor through the source string.
|
|
3397
|
-
*/
|
|
3398
|
-
function mergeReferences(refs) {
|
|
3399
|
-
const sorted = [...refs].sort((a, b) => a.start - b.start);
|
|
3400
|
-
const merged = [];
|
|
3401
|
-
let lastEnd = -1;
|
|
3402
|
-
for (const ref of sorted) {
|
|
3403
|
-
if (ref.start < lastEnd) continue;
|
|
3404
|
-
merged.push(ref);
|
|
3405
|
-
lastEnd = ref.end;
|
|
3406
|
-
}
|
|
3407
|
-
return merged;
|
|
3408
|
-
}
|
|
3409
|
-
/**
|
|
3410
|
-
* Collect every provider's references in one pass. Convenience wrapper —
|
|
3411
|
-
* the TUI textarea component calls this on every keystroke to highlight
|
|
3412
|
-
* in-prompt mentions.
|
|
3413
|
-
*/
|
|
3414
|
-
function collectReferences(text, providers, cursor = text.length) {
|
|
3415
|
-
const ctx = {
|
|
3416
|
-
text,
|
|
3417
|
-
cursor
|
|
3418
|
-
};
|
|
3419
|
-
const refs = [];
|
|
3420
|
-
for (const p of providers) for (const ref of p.parseReferences(text, ctx)) refs.push(ref);
|
|
3421
|
-
return mergeReferences(refs);
|
|
3422
|
-
}
|
|
3423
|
-
/**
|
|
3424
2505
|
* Shared sentinel returned by `useCompletion` when no references are
|
|
3425
2506
|
* present in the buffer. Stable identity lets consumers' `useEffect`
|
|
3426
2507
|
* dep arrays bail out on chip-less keystrokes — see `useChipHighlights`.
|
|
@@ -3548,258 +2629,6 @@ function useCompletion(input, providers, options = {}) {
|
|
|
3548
2629
|
]);
|
|
3549
2630
|
}
|
|
3550
2631
|
//#endregion
|
|
3551
|
-
//#region src/chat/completion-files.ts
|
|
3552
|
-
/** Trigger character — `@` is the conventional file-mention prefix in chat UIs. */
|
|
3553
|
-
const FILES_TRIGGER = "@";
|
|
3554
|
-
/** Cap on returned items. Keeps the popover compact + render-cheap. */
|
|
3555
|
-
const DEFAULT_RESULT_LIMIT = 50;
|
|
3556
|
-
/** Identity formatter — preserves the discovery path verbatim. */
|
|
3557
|
-
const IDENTITY_FORMAT = (entry) => entry.path;
|
|
3558
|
-
/**
|
|
3559
|
-
* Rank-and-slice a file catalog against a query. Hoisted to a module
|
|
3560
|
-
* helper so both the sync and async branches of `suggest()` share one
|
|
3561
|
-
* implementation (the async branch hits this once the lazy directory
|
|
3562
|
-
* walk resolves; sync branch hits it on every keystroke thereafter).
|
|
3563
|
-
*
|
|
3564
|
-
* `formatPath` rewrites the catalog's project-root-relative path into
|
|
3565
|
-
* the form the host wants emitted into the prompt (typically CWD-rel
|
|
3566
|
-
* or absolute when launched from a project subdir — see
|
|
3567
|
-
* `formatPathForCwd` in `path-display.ts`). Falls back to the raw
|
|
3568
|
-
* `entry.path` when omitted.
|
|
3569
|
-
*/
|
|
3570
|
-
function scoreFiles(catalog, query, limit, formatPath) {
|
|
3571
|
-
const q = query.trim().toLowerCase();
|
|
3572
|
-
const scored = [];
|
|
3573
|
-
for (const file of catalog) {
|
|
3574
|
-
const display = formatPath(file);
|
|
3575
|
-
const name = file.name.toLowerCase();
|
|
3576
|
-
const path = display.toLowerCase();
|
|
3577
|
-
if (q.length === 0) {
|
|
3578
|
-
scored.push({
|
|
3579
|
-
entry: file,
|
|
3580
|
-
display,
|
|
3581
|
-
rank: 4
|
|
3582
|
-
});
|
|
3583
|
-
continue;
|
|
3584
|
-
}
|
|
3585
|
-
if (name === q) {
|
|
3586
|
-
scored.push({
|
|
3587
|
-
entry: file,
|
|
3588
|
-
display,
|
|
3589
|
-
rank: 0
|
|
3590
|
-
});
|
|
3591
|
-
continue;
|
|
3592
|
-
}
|
|
3593
|
-
if (name.startsWith(q)) {
|
|
3594
|
-
scored.push({
|
|
3595
|
-
entry: file,
|
|
3596
|
-
display,
|
|
3597
|
-
rank: 1
|
|
3598
|
-
});
|
|
3599
|
-
continue;
|
|
3600
|
-
}
|
|
3601
|
-
if (name.includes(q)) {
|
|
3602
|
-
scored.push({
|
|
3603
|
-
entry: file,
|
|
3604
|
-
display,
|
|
3605
|
-
rank: 2
|
|
3606
|
-
});
|
|
3607
|
-
continue;
|
|
3608
|
-
}
|
|
3609
|
-
if (path.includes(q)) {
|
|
3610
|
-
scored.push({
|
|
3611
|
-
entry: file,
|
|
3612
|
-
display,
|
|
3613
|
-
rank: 3
|
|
3614
|
-
});
|
|
3615
|
-
continue;
|
|
3616
|
-
}
|
|
3617
|
-
}
|
|
3618
|
-
scored.sort((a, b) => {
|
|
3619
|
-
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
3620
|
-
return a.display.localeCompare(b.display);
|
|
3621
|
-
});
|
|
3622
|
-
return scored.slice(0, limit).map(({ entry, display }) => ({
|
|
3623
|
-
id: display,
|
|
3624
|
-
label: entry.name,
|
|
3625
|
-
description: parentDir(entry.path),
|
|
3626
|
-
insertText: `@${display} `,
|
|
3627
|
-
data: entry
|
|
3628
|
-
}));
|
|
3629
|
-
}
|
|
3630
|
-
/**
|
|
3631
|
-
* Build an `@`-prefixed files completion provider against a *live* catalog.
|
|
3632
|
-
*
|
|
3633
|
-
* The factory captures a getter so the catalog can be re-scanned (cwd
|
|
3634
|
-
* change, manual refresh) without re-instantiating the provider — the
|
|
3635
|
-
* App keeps one provider for the lifetime of the prompt block and just
|
|
3636
|
-
* mutates the underlying state.
|
|
3637
|
-
*
|
|
3638
|
-
* `limit` caps the result list so the popover stays bounded on huge
|
|
3639
|
-
* monorepos. Filtering is substring on `path` + `name`, case-insensitive;
|
|
3640
|
-
* ranking prefers (in order): exact name match, name prefix, name
|
|
3641
|
-
* substring, path substring, alphabetical.
|
|
3642
|
-
*/
|
|
3643
|
-
function createFilesCompletionProvider(opts) {
|
|
3644
|
-
const limit = opts.limit ?? DEFAULT_RESULT_LIMIT;
|
|
3645
|
-
const formatPath = opts.formatPath ?? IDENTITY_FORMAT;
|
|
3646
|
-
return {
|
|
3647
|
-
id: "files",
|
|
3648
|
-
trigger: "@",
|
|
3649
|
-
label: "Files",
|
|
3650
|
-
suggest(query) {
|
|
3651
|
-
if (opts.ensureCatalog) {
|
|
3652
|
-
const pending = opts.ensureCatalog();
|
|
3653
|
-
if (opts.getCatalog().length === 0) return pending.then((loaded) => scoreFiles(loaded, query, limit, formatPath));
|
|
3654
|
-
}
|
|
3655
|
-
return scoreFiles(opts.getCatalog(), query, limit, formatPath);
|
|
3656
|
-
},
|
|
3657
|
-
parseReferences(text, _ctx) {
|
|
3658
|
-
const catalog = opts.getCatalog();
|
|
3659
|
-
if (catalog.length === 0) return [];
|
|
3660
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
3661
|
-
for (const file of catalog) byPath.set(formatPath(file), file);
|
|
3662
|
-
const refs = [];
|
|
3663
|
-
for (const m of text.matchAll(/(^|\s)@(\S+)/g)) {
|
|
3664
|
-
const rawCandidate = m[2];
|
|
3665
|
-
const stripped = byPath.has(rawCandidate) ? rawCandidate : rawCandidate.replace(/[.,;:)\]}!?]+$/, "");
|
|
3666
|
-
const file = byPath.get(stripped);
|
|
3667
|
-
if (!file) continue;
|
|
3668
|
-
const start = m.index + m[1].length;
|
|
3669
|
-
const trimmedLen = 1 + stripped.length;
|
|
3670
|
-
refs.push({
|
|
3671
|
-
providerId: "files",
|
|
3672
|
-
start,
|
|
3673
|
-
end: start + trimmedLen,
|
|
3674
|
-
itemId: stripped,
|
|
3675
|
-
data: file
|
|
3676
|
-
});
|
|
3677
|
-
}
|
|
3678
|
-
return refs;
|
|
3679
|
-
}
|
|
3680
|
-
};
|
|
3681
|
-
}
|
|
3682
|
-
/** Return the parent directory of a forward-slashed path, or `''` for root entries. */
|
|
3683
|
-
function parentDir(path) {
|
|
3684
|
-
const lastSlash = path.lastIndexOf("/");
|
|
3685
|
-
return lastSlash <= 0 ? "" : path.slice(0, lastSlash);
|
|
3686
|
-
}
|
|
3687
|
-
/**
|
|
3688
|
-
* Walk a reference list and return the deduplicated set of files in
|
|
3689
|
-
* first-mention order — input to "attach these files to the prompt"
|
|
3690
|
-
* downstream logic.
|
|
3691
|
-
*/
|
|
3692
|
-
function uniqueFilesFromReferences(references) {
|
|
3693
|
-
const out = [];
|
|
3694
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3695
|
-
for (const ref of references) {
|
|
3696
|
-
if (ref.providerId !== "files") continue;
|
|
3697
|
-
if (seen.has(ref.itemId)) continue;
|
|
3698
|
-
seen.add(ref.itemId);
|
|
3699
|
-
out.push(ref.data);
|
|
3700
|
-
}
|
|
3701
|
-
return out;
|
|
3702
|
-
}
|
|
3703
|
-
//#endregion
|
|
3704
|
-
//#region src/chat/completion-skills.ts
|
|
3705
|
-
/** Trigger character — slash-commands convention. */
|
|
3706
|
-
const SKILLS_TRIGGER = "/";
|
|
3707
|
-
/** Valid skill-name shape (matches the parser): lowercase alnum + dashes. */
|
|
3708
|
-
const SKILL_NAME_RX = /^[a-z0-9][a-z0-9-]*$/;
|
|
3709
|
-
/**
|
|
3710
|
-
* Filter + rank visible skills against a query. Hoisted to a module
|
|
3711
|
-
* helper so the sync and async branches of `suggest()` share one
|
|
3712
|
-
* implementation (the async branch hits this once the lazy SKILL.md
|
|
3713
|
-
* scan resolves; sync branch hits it on every keystroke thereafter).
|
|
3714
|
-
*/
|
|
3715
|
-
function scoreSkills(catalog, query) {
|
|
3716
|
-
const q = query.trim().toLowerCase();
|
|
3717
|
-
return catalog.filter((skill) => SKILL_NAME_RX.test(skill.name)).filter((skill) => {
|
|
3718
|
-
if (q.length === 0) return true;
|
|
3719
|
-
return skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q);
|
|
3720
|
-
}).sort((a, b) => {
|
|
3721
|
-
const an = a.name.toLowerCase();
|
|
3722
|
-
const bn = b.name.toLowerCase();
|
|
3723
|
-
if (q) {
|
|
3724
|
-
const aPrefix = an.startsWith(q);
|
|
3725
|
-
if (aPrefix !== bn.startsWith(q)) return aPrefix ? -1 : 1;
|
|
3726
|
-
}
|
|
3727
|
-
return an.localeCompare(bn);
|
|
3728
|
-
}).map((skill) => ({
|
|
3729
|
-
id: skill.name,
|
|
3730
|
-
label: skill.name,
|
|
3731
|
-
description: skill.description,
|
|
3732
|
-
insertText: `/${skill.name} `,
|
|
3733
|
-
data: skill
|
|
3734
|
-
}));
|
|
3735
|
-
}
|
|
3736
|
-
/**
|
|
3737
|
-
* Build a slash-command completion provider against a *live* skills
|
|
3738
|
-
* catalog. The factory captures a getter so the catalog can change across
|
|
3739
|
-
* renders (toggles, reload) without re-instantiating the provider.
|
|
3740
|
-
*
|
|
3741
|
-
* Pass `getEnabled` to additionally hide skills the user has toggled off
|
|
3742
|
-
* — when undefined, every catalog entry is offered.
|
|
3743
|
-
*/
|
|
3744
|
-
function createSkillsCompletionProvider(opts) {
|
|
3745
|
-
const visible = () => {
|
|
3746
|
-
const all = opts.getCatalog();
|
|
3747
|
-
const enabled = opts.getEnabled?.();
|
|
3748
|
-
if (enabled === void 0) return [...all];
|
|
3749
|
-
const allow = new Set(enabled);
|
|
3750
|
-
return all.filter((s) => allow.has(s.name));
|
|
3751
|
-
};
|
|
3752
|
-
return {
|
|
3753
|
-
id: "skills",
|
|
3754
|
-
trigger: "/",
|
|
3755
|
-
label: "Skills",
|
|
3756
|
-
suggest(query) {
|
|
3757
|
-
if (opts.ensureCatalog) {
|
|
3758
|
-
const pending = opts.ensureCatalog();
|
|
3759
|
-
if (opts.getCatalog().length === 0) return pending.then(() => scoreSkills(visible(), query));
|
|
3760
|
-
}
|
|
3761
|
-
return scoreSkills(visible(), query);
|
|
3762
|
-
},
|
|
3763
|
-
parseReferences(text, _ctx) {
|
|
3764
|
-
const catalog = visible();
|
|
3765
|
-
if (catalog.length === 0) return [];
|
|
3766
|
-
const byName = /* @__PURE__ */ new Map();
|
|
3767
|
-
for (const skill of catalog) byName.set(skill.name, skill);
|
|
3768
|
-
const refs = [];
|
|
3769
|
-
for (const m of text.matchAll(/(^|\s)(\/([a-z0-9][a-z0-9-]*))/g)) {
|
|
3770
|
-
const name = m[3];
|
|
3771
|
-
const skill = byName.get(name);
|
|
3772
|
-
if (!skill) continue;
|
|
3773
|
-
const start = m.index + m[1].length;
|
|
3774
|
-
refs.push({
|
|
3775
|
-
providerId: "skills",
|
|
3776
|
-
start,
|
|
3777
|
-
end: start + m[2].length,
|
|
3778
|
-
itemId: skill.name,
|
|
3779
|
-
data: skill
|
|
3780
|
-
});
|
|
3781
|
-
}
|
|
3782
|
-
return refs;
|
|
3783
|
-
}
|
|
3784
|
-
};
|
|
3785
|
-
}
|
|
3786
|
-
/**
|
|
3787
|
-
* Walk a parsed prompt for skill references and return the deduplicated
|
|
3788
|
-
* list of skill names — input to `agent.activateSkill(name)` calls on
|
|
3789
|
-
* submit.
|
|
3790
|
-
*/
|
|
3791
|
-
function uniqueSkillNamesFromReferences(references) {
|
|
3792
|
-
const out = [];
|
|
3793
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3794
|
-
for (const ref of references) {
|
|
3795
|
-
if (ref.providerId !== "skills") continue;
|
|
3796
|
-
if (seen.has(ref.itemId)) continue;
|
|
3797
|
-
seen.add(ref.itemId);
|
|
3798
|
-
out.push(ref.itemId);
|
|
3799
|
-
}
|
|
3800
|
-
return out;
|
|
3801
|
-
}
|
|
3802
|
-
//#endregion
|
|
3803
2632
|
//#region src/chat/keybindings.ts
|
|
3804
2633
|
/**
|
|
3805
2634
|
* Keybindings — a single source of truth for the app's global shortcuts.
|
|
@@ -3859,8 +2688,8 @@ const KEYBINDING_DEFS = [
|
|
|
3859
2688
|
{
|
|
3860
2689
|
action: "openEffortPicker",
|
|
3861
2690
|
default: "ctrl+l",
|
|
3862
|
-
label: "
|
|
3863
|
-
description: "open the
|
|
2691
|
+
label: "options",
|
|
2692
|
+
description: "open the model-options picker — reasoning effort + feature toggles (e.g. fast mode)",
|
|
3864
2693
|
group: "Global"
|
|
3865
2694
|
},
|
|
3866
2695
|
{
|
|
@@ -3870,6 +2699,13 @@ const KEYBINDING_DEFS = [
|
|
|
3870
2699
|
description: "open the active run's todo list (the agent's `todowrite` checkpoints)",
|
|
3871
2700
|
group: "Global"
|
|
3872
2701
|
},
|
|
2702
|
+
{
|
|
2703
|
+
action: "openContextPanel",
|
|
2704
|
+
default: "ctrl+g",
|
|
2705
|
+
label: "context",
|
|
2706
|
+
description: "open the context-usage breakdown (what is taking up the context window)",
|
|
2707
|
+
group: "Global"
|
|
2708
|
+
},
|
|
3873
2709
|
{
|
|
3874
2710
|
action: "openKeybindings",
|
|
3875
2711
|
default: "ctrl+y",
|
|
@@ -4253,750 +3089,55 @@ function renderDefaultFile() {
|
|
|
4253
3089
|
});
|
|
4254
3090
|
lines.push("}");
|
|
4255
3091
|
lines.push("");
|
|
4256
|
-
return lines.join("\n");
|
|
4257
|
-
}
|
|
4258
|
-
/**
|
|
4259
|
-
* Strip `//` line comments and `/* … *\/` block comments from a JSONC
|
|
4260
|
-
* source string. Stays out of string literals so a `"foo // bar"` value
|
|
4261
|
-
* keeps its slashes. Exported for unit-tests; the only runtime caller
|
|
4262
|
-
* is {@link readKeybindings}.
|
|
4263
|
-
*
|
|
4264
|
-
* Trade-off: this is a tiny state-machine, not a full JSONC parser. It
|
|
4265
|
-
* doesn't understand `\\` outside strings (irrelevant for JSON) and
|
|
4266
|
-
* doesn't track line numbers in error messages. Good enough for a
|
|
4267
|
-
* config file — corruption in production gracefully falls back to the
|
|
4268
|
-
* defaults via the try/catch in `readKeybindings`.
|
|
4269
|
-
*/
|
|
4270
|
-
function stripJsonComments(input) {
|
|
4271
|
-
let out = "";
|
|
4272
|
-
let i = 0;
|
|
4273
|
-
let inString = false;
|
|
4274
|
-
let escape = false;
|
|
4275
|
-
while (i < input.length) {
|
|
4276
|
-
const c = input[i];
|
|
4277
|
-
if (inString) {
|
|
4278
|
-
out += c;
|
|
4279
|
-
if (escape) escape = false;
|
|
4280
|
-
else if (c === "\\") escape = true;
|
|
4281
|
-
else if (c === "\"") inString = false;
|
|
4282
|
-
i++;
|
|
4283
|
-
continue;
|
|
4284
|
-
}
|
|
4285
|
-
if (c === "\"") {
|
|
4286
|
-
inString = true;
|
|
4287
|
-
out += c;
|
|
4288
|
-
i++;
|
|
4289
|
-
continue;
|
|
4290
|
-
}
|
|
4291
|
-
if (c === "/" && input[i + 1] === "/") {
|
|
4292
|
-
while (i < input.length && input[i] !== "\n") i++;
|
|
4293
|
-
continue;
|
|
4294
|
-
}
|
|
4295
|
-
if (c === "/" && input[i + 1] === "*") {
|
|
4296
|
-
i += 2;
|
|
4297
|
-
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
|
|
4298
|
-
i += 2;
|
|
4299
|
-
continue;
|
|
4300
|
-
}
|
|
4301
|
-
out += c;
|
|
4302
|
-
i++;
|
|
4303
|
-
}
|
|
4304
|
-
return out;
|
|
4305
|
-
}
|
|
4306
|
-
//#endregion
|
|
4307
|
-
//#region src/chat/edit-approval.ts
|
|
4308
|
-
/**
|
|
4309
|
-
* Convert a per-hunk approval mask into an `EditOutcome[]`. `true` →
|
|
4310
|
-
* `applied`; `false` → `denied` with the supplied reason.
|
|
4311
|
-
*
|
|
4312
|
-
* Length is `Math.max(mask.length, fallbackLength)` so callers passing a
|
|
4313
|
-
* shorter mask still get a fully-populated array — missing entries
|
|
4314
|
-
* default to applied, matching the "no decision => keep" convention.
|
|
4315
|
-
*/
|
|
4316
|
-
function maskToOutcomeKinds(mask, fallbackLength, deniedReason = "denied by user") {
|
|
4317
|
-
const len = Math.max(mask.length, fallbackLength);
|
|
4318
|
-
const out = [];
|
|
4319
|
-
for (let i = 0; i < len; i++) {
|
|
4320
|
-
const keep = i < mask.length ? mask[i] : true;
|
|
4321
|
-
out.push(keep ? { kind: "applied" } : {
|
|
4322
|
-
kind: "denied",
|
|
4323
|
-
reason: deniedReason
|
|
4324
|
-
});
|
|
4325
|
-
}
|
|
4326
|
-
return out;
|
|
4327
|
-
}
|
|
4328
|
-
function resolveApprovalForPayload(decision, payload) {
|
|
4329
|
-
const total = payload.hunks.length;
|
|
4330
|
-
if (decision === "deny") {
|
|
4331
|
-
const outcomes = Array.from({ length: total }, () => ({
|
|
4332
|
-
kind: "denied",
|
|
4333
|
-
reason: "denied by user"
|
|
4334
|
-
}));
|
|
4335
|
-
return {
|
|
4336
|
-
outcomes,
|
|
4337
|
-
shouldBlock: true,
|
|
4338
|
-
syntheticEvent: {
|
|
4339
|
-
...payload,
|
|
4340
|
-
outcomes
|
|
4341
|
-
}
|
|
4342
|
-
};
|
|
4343
|
-
}
|
|
4344
|
-
if (typeof decision === "object" && decision.kind === "partial") {
|
|
4345
|
-
const outcomes = maskToOutcomeKinds(decision.mask, total);
|
|
4346
|
-
return {
|
|
4347
|
-
outcomes,
|
|
4348
|
-
shouldBlock: !outcomes.some((o) => o.kind === "applied"),
|
|
4349
|
-
syntheticEvent: {
|
|
4350
|
-
...payload,
|
|
4351
|
-
outcomes
|
|
4352
|
-
}
|
|
4353
|
-
};
|
|
4354
|
-
}
|
|
4355
|
-
return {
|
|
4356
|
-
outcomes: Array.from({ length: total }, () => ({ kind: "applied" })),
|
|
4357
|
-
shouldBlock: false,
|
|
4358
|
-
syntheticEvent: null
|
|
4359
|
-
};
|
|
4360
|
-
}
|
|
4361
|
-
/** Sentinel tags used by {@link buildEditOutcomesAnnotation} / parser. */
|
|
4362
|
-
const ANNOTATION_OPEN = "<edit-outcomes>";
|
|
4363
|
-
const ANNOTATION_CLOSE = "</edit-outcomes>";
|
|
4364
|
-
const OUTCOME_KIND_RE = /^applied|denied|skipped|failed$/;
|
|
4365
|
-
const OUTCOME_LINE_RE = /^#(\d+) (applied|denied|skipped|failed)(?:: ?(.*))?$/;
|
|
4366
|
-
/**
|
|
4367
|
-
* Render an `EditOutcome[]` as the wire-format annotation block. Returns
|
|
4368
|
-
* the body to APPEND to a tool result; callers join with a leading
|
|
4369
|
-
* `\n\n` separator. Idempotent on missing reasons — bare `applied` lines
|
|
4370
|
-
* stay terse.
|
|
4371
|
-
*/
|
|
4372
|
-
function buildEditOutcomesAnnotation(outcomes) {
|
|
4373
|
-
const lines = [ANNOTATION_OPEN];
|
|
4374
|
-
for (let i = 0; i < outcomes.length; i++) {
|
|
4375
|
-
const o = outcomes[i];
|
|
4376
|
-
if (!OUTCOME_KIND_RE.test(o.kind)) continue;
|
|
4377
|
-
const reason = o.reason ? `: ${o.reason}` : "";
|
|
4378
|
-
lines.push(`#${i + 1} ${o.kind}${reason}`);
|
|
4379
|
-
}
|
|
4380
|
-
lines.push(ANNOTATION_CLOSE);
|
|
4381
|
-
return lines.join("\n");
|
|
4382
|
-
}
|
|
4383
|
-
/**
|
|
4384
|
-
* Parse an `<edit-outcomes>…</edit-outcomes>` annotation block out of a
|
|
4385
|
-
* tool result body. Returns the outcomes keyed by 1-based hunk index, or
|
|
4386
|
-
* `null` when the block is missing / malformed.
|
|
4387
|
-
*
|
|
4388
|
-
* Anchored on the explicit tag pair so the parser doesn't false-positive
|
|
4389
|
-
* on natural prose that happens to contain `#1 applied`.
|
|
4390
|
-
*/
|
|
4391
|
-
function parseEditOutcomesFromResult(result) {
|
|
4392
|
-
const text = typeof result === "string" ? result : result.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
4393
|
-
if (!text) return null;
|
|
4394
|
-
const openIdx = text.indexOf(`\n${ANNOTATION_OPEN}\n`);
|
|
4395
|
-
const startIdx = openIdx >= 0 ? openIdx + 1 : text.startsWith(`${ANNOTATION_OPEN}\n`) ? 0 : -1;
|
|
4396
|
-
if (startIdx < 0) return null;
|
|
4397
|
-
const closeNeedle = `\n${ANNOTATION_CLOSE}`;
|
|
4398
|
-
const closeIdx = text.indexOf(closeNeedle, startIdx);
|
|
4399
|
-
if (closeIdx < 0) return null;
|
|
4400
|
-
const body = text.slice(startIdx + 15 + 1, closeIdx);
|
|
4401
|
-
const found = [];
|
|
4402
|
-
for (const line of body.split("\n")) {
|
|
4403
|
-
if (line.length === 0) continue;
|
|
4404
|
-
const m = OUTCOME_LINE_RE.exec(line);
|
|
4405
|
-
if (!m) return null;
|
|
4406
|
-
const idx = Number.parseInt(m[1], 10);
|
|
4407
|
-
if (!Number.isFinite(idx) || idx < 1) return null;
|
|
4408
|
-
const kind = m[2];
|
|
4409
|
-
const reason = m[3]?.trim();
|
|
4410
|
-
found.push({
|
|
4411
|
-
idx,
|
|
4412
|
-
outcome: {
|
|
4413
|
-
kind,
|
|
4414
|
-
...reason ? { reason } : {}
|
|
4415
|
-
}
|
|
4416
|
-
});
|
|
4417
|
-
}
|
|
4418
|
-
if (found.length === 0) return null;
|
|
4419
|
-
const maxIdx = Math.max(...found.map((f) => f.idx));
|
|
4420
|
-
const outcomes = Array.from({ length: maxIdx }, () => ({ kind: "applied" }));
|
|
4421
|
-
for (const { idx, outcome } of found) outcomes[idx - 1] = outcome;
|
|
4422
|
-
return outcomes;
|
|
4423
|
-
}
|
|
4424
|
-
/**
|
|
4425
|
-
* Strip the first `<edit-outcomes>…</edit-outcomes>` block out of a tool
|
|
4426
|
-
* result body, returning the surrounding text. Used by the
|
|
4427
|
-
* `tool:transform` hook to peel a body-emitted annotation before
|
|
4428
|
-
* re-appending the merged (approval ∪ body) version — otherwise the
|
|
4429
|
-
* result would carry two annotation blocks and
|
|
4430
|
-
* {@link parseEditOutcomesFromResult} would only see the first.
|
|
4431
|
-
*
|
|
4432
|
-
* Anchored on the same `\n<edit-outcomes>\n` / start-of-string newline
|
|
4433
|
-
* shape the parser uses, so prose that incidentally mentions
|
|
4434
|
-
* `<edit-outcomes>` (e.g. a model summarizing its own format) isn't
|
|
4435
|
-
* mistakenly stripped. Trims a single leading `\n\n` separator when
|
|
4436
|
-
* present so successive strips don't leave dangling blank lines.
|
|
4437
|
-
* Idempotent on inputs that don't contain a properly-anchored block.
|
|
4438
|
-
*/
|
|
4439
|
-
function stripEditOutcomesAnnotation(text) {
|
|
4440
|
-
const newlineNeedle = `\n${ANNOTATION_OPEN}\n`;
|
|
4441
|
-
const newlineIdx = text.indexOf(newlineNeedle);
|
|
4442
|
-
let openIdx;
|
|
4443
|
-
if (newlineIdx >= 0) openIdx = newlineIdx + 1;
|
|
4444
|
-
else if (text.startsWith(`${ANNOTATION_OPEN}\n`)) openIdx = 0;
|
|
4445
|
-
else return text;
|
|
4446
|
-
const closeIdx = text.indexOf(ANNOTATION_CLOSE, openIdx);
|
|
4447
|
-
if (closeIdx < 0) return text;
|
|
4448
|
-
const blockEnd = closeIdx + 16;
|
|
4449
|
-
const sepStart = openIdx >= 2 && text.slice(openIdx - 2, openIdx) === "\n\n" ? openIdx - 2 : openIdx;
|
|
4450
|
-
return text.slice(0, sepStart) + text.slice(blockEnd);
|
|
4451
|
-
}
|
|
4452
|
-
/**
|
|
4453
|
-
* Merge body-side outcomes (keyed against the approved subset the tool
|
|
4454
|
-
* actually ran on, in subset-position order) into approval-side outcomes
|
|
4455
|
-
* (1:1 with the model's ORIGINAL `edits` list, with `denied` entries for
|
|
4456
|
-
* every hunk the user dropped).
|
|
4457
|
-
*
|
|
4458
|
-
* Algorithm: walk the approval array; every `applied` placeholder
|
|
4459
|
-
* corresponds to one approved hunk that the body ran. Consume body's
|
|
4460
|
-
* outcomes in order against those placeholders. Non-`applied` approval
|
|
4461
|
-
* entries (`denied`, `skipped`) stay untouched — they describe gate-
|
|
4462
|
-
* level decisions the body never saw.
|
|
4463
|
-
*
|
|
4464
|
-
* Pure. Returns a fresh array; never mutates either input.
|
|
4465
|
-
*
|
|
4466
|
-
* Edge cases:
|
|
4467
|
-
* - `body` is empty / shorter than the approved count → remaining
|
|
4468
|
-
* approval `applied` placeholders stay as `applied` (the body ran
|
|
4469
|
-
* happily; absence of a body entry means nothing failed).
|
|
4470
|
-
* - `body` longer than approved count → trailing body entries are
|
|
4471
|
-
* ignored. Shouldn't happen in practice (body sees the rebound
|
|
4472
|
-
* subset), but the guard keeps the merge total-pure.
|
|
4473
|
-
*/
|
|
4474
|
-
function mergeApprovalAndBodyOutcomes(approval, body) {
|
|
4475
|
-
if (!body || body.length === 0) return approval.slice();
|
|
4476
|
-
const out = [];
|
|
4477
|
-
let bi = 0;
|
|
4478
|
-
for (const entry of approval) if (entry.kind === "applied" && bi < body.length) {
|
|
4479
|
-
out.push(body[bi]);
|
|
4480
|
-
bi++;
|
|
4481
|
-
} else out.push(entry);
|
|
4482
|
-
return out;
|
|
4483
|
-
}
|
|
4484
|
-
/**
|
|
4485
|
-
* Rewrite a `multi_edit` body header so the totals reflect the model's
|
|
4486
|
-
* ORIGINAL edit list (the merged outcomes count) instead of the subset
|
|
4487
|
-
* the body actually saw after gate rebinding. Without this, a partially
|
|
4488
|
-
* approved call surfaces a misleading `applied 2 of 2 edits` (subset
|
|
4489
|
-
* counts) on the wire even when the original was `applied 2 of 3`.
|
|
4490
|
-
*
|
|
4491
|
-
* Three body-side shapes are handled (matching `multi_edit`'s emit):
|
|
4492
|
-
* 1. `Edited <path>: applied N edits (R replacements).`
|
|
4493
|
-
* 2. `Edited <path>: applied N of M edits (R replacements).`
|
|
4494
|
-
* 3. `multi_edit error: no edits applied to <path> (M attempted).`
|
|
4495
|
-
*
|
|
4496
|
-
* The replacements count is preserved verbatim — it's a body-side stat
|
|
4497
|
-
* the chat layer can't recompute. When the first line doesn't look like
|
|
4498
|
-
* any of the three shapes (e.g. an unrelated error preamble bubbled up),
|
|
4499
|
-
* the text is returned unchanged.
|
|
4500
|
-
*/
|
|
4501
|
-
function rewriteMultiEditHeader(text, merged, path) {
|
|
4502
|
-
const newlineIdx = text.indexOf("\n");
|
|
4503
|
-
const firstLine = newlineIdx < 0 ? text : text.slice(0, newlineIdx);
|
|
4504
|
-
const rest = newlineIdx < 0 ? "" : text.slice(newlineIdx);
|
|
4505
|
-
const successMatch = firstLine.match(/^Edited .+: applied \d+(?: of \d+)? edits? \((\d+) replacement/);
|
|
4506
|
-
const isFailedShape = firstLine.startsWith("multi_edit error: no edits applied to ") && firstLine.endsWith(" attempted).");
|
|
4507
|
-
if (!successMatch && !isFailedShape) return text;
|
|
4508
|
-
const replacements = successMatch ? Number.parseInt(successMatch[1], 10) || 0 : 0;
|
|
4509
|
-
const applied = summarizeOutcomes(merged).applied;
|
|
4510
|
-
const total = merged.length;
|
|
4511
|
-
let newHeader;
|
|
4512
|
-
if (applied === total) newHeader = `Edited ${path}: applied ${total} edit${total === 1 ? "" : "s"} (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
|
|
4513
|
-
else if (applied > 0) newHeader = `Edited ${path}: applied ${applied} of ${total} edits (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
|
|
4514
|
-
else newHeader = `multi_edit error: no edits applied to ${path} (${total} attempted).`;
|
|
4515
|
-
return newHeader + rest;
|
|
4516
|
-
}
|
|
4517
|
-
/**
|
|
4518
|
-
* Aggregate counts for the transcript's summary badge (`3 applied · 1
|
|
4519
|
-
* denied · 1 skipped`). Exported so renderers don't reimplement the
|
|
4520
|
-
* tally. Pure / O(n).
|
|
4521
|
-
*/
|
|
4522
|
-
function summarizeOutcomes(outcomes) {
|
|
4523
|
-
const counts = {
|
|
4524
|
-
applied: 0,
|
|
4525
|
-
denied: 0,
|
|
4526
|
-
skipped: 0,
|
|
4527
|
-
failed: 0,
|
|
4528
|
-
pending: 0
|
|
4529
|
-
};
|
|
4530
|
-
if (!outcomes) return {
|
|
4531
|
-
...counts,
|
|
4532
|
-
total: 0
|
|
4533
|
-
};
|
|
4534
|
-
for (const o of outcomes) counts[o.kind] += 1;
|
|
4535
|
-
return {
|
|
4536
|
-
...counts,
|
|
4537
|
-
total: outcomes.length
|
|
4538
|
-
};
|
|
4539
|
-
}
|
|
4540
|
-
//#endregion
|
|
4541
|
-
//#region src/chat/edit-diff.ts
|
|
4542
|
-
function extractEditPayload(name, input, priorContent) {
|
|
4543
|
-
const path = input.path;
|
|
4544
|
-
if (typeof path !== "string" || path === "") return void 0;
|
|
4545
|
-
if (name === "edit") {
|
|
4546
|
-
const oldString = input.old_string;
|
|
4547
|
-
const newString = input.new_string;
|
|
4548
|
-
if (typeof oldString !== "string" || typeof newString !== "string") return void 0;
|
|
4549
|
-
return {
|
|
4550
|
-
tool: "edit",
|
|
4551
|
-
path,
|
|
4552
|
-
hunks: [{
|
|
4553
|
-
oldString,
|
|
4554
|
-
newString,
|
|
4555
|
-
...input.replace_all === true ? { replaceAll: true } : {}
|
|
4556
|
-
}],
|
|
4557
|
-
...priorContent !== void 0 ? { priorContent } : {}
|
|
4558
|
-
};
|
|
4559
|
-
}
|
|
4560
|
-
if (name === "multi_edit") {
|
|
4561
|
-
const steps = input.edits;
|
|
4562
|
-
if (!Array.isArray(steps) || steps.length === 0) return void 0;
|
|
4563
|
-
const hunks = [];
|
|
4564
|
-
for (const raw of steps) {
|
|
4565
|
-
if (typeof raw?.old_string !== "string" || typeof raw?.new_string !== "string") return void 0;
|
|
4566
|
-
hunks.push({
|
|
4567
|
-
oldString: raw.old_string,
|
|
4568
|
-
newString: raw.new_string,
|
|
4569
|
-
...raw.replace_all === true ? { replaceAll: true } : {}
|
|
4570
|
-
});
|
|
4571
|
-
}
|
|
4572
|
-
return {
|
|
4573
|
-
tool: "multi_edit",
|
|
4574
|
-
path,
|
|
4575
|
-
hunks,
|
|
4576
|
-
...priorContent !== void 0 ? { priorContent } : {}
|
|
4577
|
-
};
|
|
4578
|
-
}
|
|
4579
|
-
if (name === "write_file") {
|
|
4580
|
-
const content = input.content;
|
|
4581
|
-
if (typeof content !== "string") return void 0;
|
|
4582
|
-
return {
|
|
4583
|
-
tool: "write_file",
|
|
4584
|
-
path,
|
|
4585
|
-
hunks: [{
|
|
4586
|
-
oldString: priorContent ?? "",
|
|
4587
|
-
newString: content
|
|
4588
|
-
}],
|
|
4589
|
-
...priorContent !== void 0 ? { priorContent } : {}
|
|
4590
|
-
};
|
|
4591
|
-
}
|
|
4592
|
-
}
|
|
4593
|
-
function computeLineDiff(oldString, newString) {
|
|
4594
|
-
const oldLines = splitLines(oldString);
|
|
4595
|
-
const newLines = splitLines(newString);
|
|
4596
|
-
const n = oldLines.length;
|
|
4597
|
-
const m = newLines.length;
|
|
4598
|
-
const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
|
|
4599
|
-
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldLines[i] === newLines[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
|
|
4600
|
-
const out = [];
|
|
4601
|
-
let i = n;
|
|
4602
|
-
let j = m;
|
|
4603
|
-
while (i > 0 || j > 0) {
|
|
4604
|
-
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
4605
|
-
out.push({
|
|
4606
|
-
op: "context",
|
|
4607
|
-
text: oldLines[i - 1]
|
|
4608
|
-
});
|
|
4609
|
-
i--;
|
|
4610
|
-
j--;
|
|
4611
|
-
continue;
|
|
4612
|
-
}
|
|
4613
|
-
if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
4614
|
-
out.push({
|
|
4615
|
-
op: "add",
|
|
4616
|
-
text: newLines[j - 1]
|
|
4617
|
-
});
|
|
4618
|
-
j--;
|
|
4619
|
-
continue;
|
|
4620
|
-
}
|
|
4621
|
-
out.push({
|
|
4622
|
-
op: "remove",
|
|
4623
|
-
text: oldLines[i - 1]
|
|
4624
|
-
});
|
|
4625
|
-
i--;
|
|
4626
|
-
}
|
|
4627
|
-
out.reverse();
|
|
4628
|
-
return out;
|
|
4629
|
-
}
|
|
4630
|
-
/**
|
|
4631
|
-
* Split a string into lines preserving empty lines but dropping the
|
|
4632
|
-
* implicit trailing `""` produced by a final `\n`. Exported only for
|
|
4633
|
-
* its tests — callers should use `computeLineDiff`.
|
|
4634
|
-
*/
|
|
4635
|
-
function splitLines(s) {
|
|
4636
|
-
if (s === "") return [];
|
|
4637
|
-
const parts = s.split("\n");
|
|
4638
|
-
if (parts[parts.length - 1] === "") parts.pop();
|
|
4639
|
-
return parts;
|
|
4640
|
-
}
|
|
4641
|
-
function computeInlineDiff(oldLine, newLine) {
|
|
4642
|
-
const oldTokens = tokenize(oldLine);
|
|
4643
|
-
const newTokens = tokenize(newLine);
|
|
4644
|
-
const n = oldTokens.length;
|
|
4645
|
-
const m = newTokens.length;
|
|
4646
|
-
const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
|
|
4647
|
-
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldTokens[i] === newTokens[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
|
|
4648
|
-
const oldSegments = [];
|
|
4649
|
-
const newSegments = [];
|
|
4650
|
-
let i = n;
|
|
4651
|
-
let j = m;
|
|
4652
|
-
while (i > 0 || j > 0) {
|
|
4653
|
-
if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
|
|
4654
|
-
pushSegment(oldSegments, {
|
|
4655
|
-
text: oldTokens[i - 1],
|
|
4656
|
-
changed: false
|
|
4657
|
-
});
|
|
4658
|
-
pushSegment(newSegments, {
|
|
4659
|
-
text: newTokens[j - 1],
|
|
4660
|
-
changed: false
|
|
4661
|
-
});
|
|
4662
|
-
i--;
|
|
4663
|
-
j--;
|
|
4664
|
-
continue;
|
|
4665
|
-
}
|
|
4666
|
-
if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
4667
|
-
pushSegment(newSegments, {
|
|
4668
|
-
text: newTokens[j - 1],
|
|
4669
|
-
changed: true
|
|
4670
|
-
});
|
|
4671
|
-
j--;
|
|
4672
|
-
continue;
|
|
4673
|
-
}
|
|
4674
|
-
pushSegment(oldSegments, {
|
|
4675
|
-
text: oldTokens[i - 1],
|
|
4676
|
-
changed: true
|
|
4677
|
-
});
|
|
4678
|
-
i--;
|
|
4679
|
-
}
|
|
4680
|
-
oldSegments.reverse();
|
|
4681
|
-
newSegments.reverse();
|
|
4682
|
-
return {
|
|
4683
|
-
oldSegments,
|
|
4684
|
-
newSegments
|
|
4685
|
-
};
|
|
4686
|
-
}
|
|
4687
|
-
/**
|
|
4688
|
-
* Coalesce adjacent same-state segments so the renderer emits one
|
|
4689
|
-
* `<span>` per run instead of one per token — keeps the React tree
|
|
4690
|
-
* shallow on dense lines without changing the visual output.
|
|
4691
|
-
*
|
|
4692
|
-
* Walking direction is reverse (we push during the reverse walk, then
|
|
4693
|
-
* the caller reverses the array), so we coalesce against the *tail*.
|
|
4694
|
-
*/
|
|
4695
|
-
function pushSegment(buf, seg) {
|
|
4696
|
-
const tail = buf[buf.length - 1];
|
|
4697
|
-
if (tail && tail.changed === seg.changed) tail.text = seg.text + tail.text;
|
|
4698
|
-
else buf.push(seg);
|
|
4699
|
-
}
|
|
4700
|
-
/**
|
|
4701
|
-
* Tokenize on word / non-word boundaries. Each run of `\w+` is one
|
|
4702
|
-
* token; each run of `\W+` (whitespace, punctuation) is another. This
|
|
4703
|
-
* gives the right granularity for renames (`oldName` → `newName`) and
|
|
4704
|
-
* for symbol swaps (`+ → -`) without exploding into per-char segments.
|
|
4705
|
-
*
|
|
4706
|
-
* Exported only for its tests.
|
|
4707
|
-
*/
|
|
4708
|
-
function tokenize(s) {
|
|
4709
|
-
if (s === "") return [];
|
|
4710
|
-
const out = [];
|
|
4711
|
-
for (const match of s.matchAll(/\w+|\W+/g)) out.push(match[0]);
|
|
4712
|
-
return out;
|
|
4713
|
-
}
|
|
4714
|
-
/**
|
|
4715
|
-
* Apply the payload's hunks against `priorContent` and return the
|
|
4716
|
-
* resulting file body. Mirrors the agent's tool-side semantics:
|
|
4717
|
-
* - `replaceAll === true` → `String.replaceAll`
|
|
4718
|
-
* - otherwise → first-occurrence `String.replace`
|
|
4719
|
-
*
|
|
4720
|
-
* Hunks are applied in order — a `multi_edit` later hunk operates on
|
|
4721
|
-
* the output of the earlier ones, just like the actual tool.
|
|
4722
|
-
*/
|
|
4723
|
-
function applyEditPayload(payload, priorContent) {
|
|
4724
|
-
let out = priorContent;
|
|
4725
|
-
for (const hunk of payload.hunks) out = hunk.replaceAll ? out.replaceAll(hunk.oldString, hunk.newString) : out.replace(hunk.oldString, hunk.newString);
|
|
4726
|
-
return out;
|
|
4727
|
-
}
|
|
4728
|
-
/**
|
|
4729
|
-
* Like `buildUnifiedDiff` but operating against the full file content
|
|
4730
|
-
* so the diff carries *real* file line numbers and configurable
|
|
4731
|
-
* surrounding context.
|
|
4732
|
-
*
|
|
4733
|
-
* Strategy:
|
|
4734
|
-
* 1. Apply the payload to `priorContent` → `newContent`.
|
|
4735
|
-
* 2. Run `computeLineDiff` over the whole file.
|
|
4736
|
-
* 3. Group non-context ops into hunks, padding each with up to
|
|
4737
|
-
* `contextLines` of context above and below. Adjacent hunks
|
|
4738
|
-
* whose context regions touch are merged so we don't emit two
|
|
4739
|
-
* `@@` headers separated by zero context lines.
|
|
4740
|
-
*
|
|
4741
|
-
* The output line numbers in the `@@` header are 1-based and reflect
|
|
4742
|
-
* the change's position in the actual file — what the user expects
|
|
4743
|
-
* when reading a diff alongside their editor.
|
|
4744
|
-
*
|
|
4745
|
-
* For `write_file` creating a new file (priorContent === ''), this
|
|
4746
|
-
* falls back to the same `--- /dev/null` convention as
|
|
4747
|
-
* `buildUnifiedDiff`.
|
|
4748
|
-
*/
|
|
4749
|
-
function buildContextualDiff(payload, priorContent, contextLines = 3) {
|
|
4750
|
-
const newContent = applyEditPayload(payload, priorContent);
|
|
4751
|
-
const isNewFile = priorContent === "";
|
|
4752
|
-
const ops = computeLineDiff(priorContent, newContent);
|
|
4753
|
-
const oldLineFor = [];
|
|
4754
|
-
const newLineFor = [];
|
|
4755
|
-
let ol = 1;
|
|
4756
|
-
let nl = 1;
|
|
4757
|
-
for (const op of ops) {
|
|
4758
|
-
oldLineFor.push(ol);
|
|
4759
|
-
newLineFor.push(nl);
|
|
4760
|
-
if (op.op !== "add") ol++;
|
|
4761
|
-
if (op.op !== "remove") nl++;
|
|
4762
|
-
}
|
|
4763
|
-
const hunks = [];
|
|
4764
|
-
for (let i = 0; i < ops.length; i++) {
|
|
4765
|
-
if (ops[i].op === "context") continue;
|
|
4766
|
-
const start = Math.max(0, i - contextLines);
|
|
4767
|
-
const end = Math.min(ops.length - 1, i + contextLines);
|
|
4768
|
-
const last = hunks[hunks.length - 1];
|
|
4769
|
-
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
4770
|
-
else hunks.push([start, end]);
|
|
4771
|
-
}
|
|
4772
|
-
if (hunks.length === 0) return "";
|
|
4773
|
-
const parts = [];
|
|
4774
|
-
parts.push(isNewFile ? "--- /dev/null" : `--- a/${payload.path}`);
|
|
4775
|
-
parts.push(`+++ b/${payload.path}`);
|
|
4776
|
-
for (const [start, end] of hunks) {
|
|
4777
|
-
const slice = ops.slice(start, end + 1);
|
|
4778
|
-
const oldCount = slice.filter((l) => l.op !== "add").length;
|
|
4779
|
-
const newCount = slice.filter((l) => l.op !== "remove").length;
|
|
4780
|
-
const oldStart = oldCount === 0 ? Math.max(0, oldLineFor[start] - 1) : oldLineFor[start];
|
|
4781
|
-
const newStart = newCount === 0 ? Math.max(0, newLineFor[start] - 1) : newLineFor[start];
|
|
4782
|
-
parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
4783
|
-
for (const line of slice) {
|
|
4784
|
-
const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
|
|
4785
|
-
parts.push(`${prefix}${line.text}`);
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
|
-
return `${parts.join("\n")}\n`;
|
|
4789
|
-
}
|
|
4790
|
-
/**
|
|
4791
|
-
* Build a per-hunk digest used by the compact diff view.
|
|
4792
|
-
*
|
|
4793
|
-
* Strategy:
|
|
4794
|
-
* - When `priorContent` is present and the payload describes a real
|
|
4795
|
-
* file transformation, compute the contextual diff once, then walk
|
|
4796
|
-
* the LCS ops splitting at runs of `add` / `remove` to anchor each
|
|
4797
|
-
* summary entry to the **real** file line. This guarantees the
|
|
4798
|
-
* summary's `L<n>` matches what the user sees in their editor.
|
|
4799
|
-
* - Otherwise, fall back to per-hunk LCS over the (oldString,
|
|
4800
|
-
* newString) snippet pair. Line numbers are absent because the
|
|
4801
|
-
* snippet has no file position.
|
|
4802
|
-
*/
|
|
4803
|
-
function summarizeEditPayload(payload) {
|
|
4804
|
-
const prior = payload.priorContent;
|
|
4805
|
-
if (prior !== void 0) return summarizeOpsByHunk(computeLineDiff(prior, applyEditPayload(payload, prior)));
|
|
4806
|
-
const hunks = [];
|
|
4807
|
-
let totalAdded = 0;
|
|
4808
|
-
let totalRemoved = 0;
|
|
4809
|
-
for (const hunk of payload.hunks) {
|
|
4810
|
-
const ops = computeLineDiff(hunk.oldString, hunk.newString);
|
|
4811
|
-
let added = 0;
|
|
4812
|
-
let removed = 0;
|
|
4813
|
-
let firstOld;
|
|
4814
|
-
let firstNew;
|
|
4815
|
-
for (const op of ops) if (op.op === "add") {
|
|
4816
|
-
added++;
|
|
4817
|
-
if (firstNew === void 0) firstNew = op.text;
|
|
4818
|
-
} else if (op.op === "remove") {
|
|
4819
|
-
removed++;
|
|
4820
|
-
if (firstOld === void 0) firstOld = op.text;
|
|
4821
|
-
}
|
|
4822
|
-
totalAdded += added;
|
|
4823
|
-
totalRemoved += removed;
|
|
4824
|
-
hunks.push({
|
|
4825
|
-
added,
|
|
4826
|
-
removed,
|
|
4827
|
-
...firstOld !== void 0 ? { firstOld } : {},
|
|
4828
|
-
...firstNew !== void 0 ? { firstNew } : {}
|
|
4829
|
-
});
|
|
4830
|
-
}
|
|
4831
|
-
return {
|
|
4832
|
-
totalAdded,
|
|
4833
|
-
totalRemoved,
|
|
4834
|
-
hunks
|
|
4835
|
-
};
|
|
3092
|
+
return lines.join("\n");
|
|
4836
3093
|
}
|
|
4837
3094
|
/**
|
|
4838
|
-
*
|
|
4839
|
-
*
|
|
4840
|
-
*
|
|
4841
|
-
*
|
|
3095
|
+
* Strip `//` line comments and `/* … *\/` block comments from a JSONC
|
|
3096
|
+
* source string. Stays out of string literals so a `"foo // bar"` value
|
|
3097
|
+
* keeps its slashes. Exported for unit-tests; the only runtime caller
|
|
3098
|
+
* is {@link readKeybindings}.
|
|
3099
|
+
*
|
|
3100
|
+
* Trade-off: this is a tiny state-machine, not a full JSONC parser. It
|
|
3101
|
+
* doesn't understand `\\` outside strings (irrelevant for JSON) and
|
|
3102
|
+
* doesn't track line numbers in error messages. Good enough for a
|
|
3103
|
+
* config file — corruption in production gracefully falls back to the
|
|
3104
|
+
* defaults via the try/catch in `readKeybindings`.
|
|
4842
3105
|
*/
|
|
4843
|
-
function
|
|
4844
|
-
|
|
4845
|
-
let totalAdded = 0;
|
|
4846
|
-
let totalRemoved = 0;
|
|
4847
|
-
let nl = 1;
|
|
3106
|
+
function stripJsonComments(input) {
|
|
3107
|
+
let out = "";
|
|
4848
3108
|
let i = 0;
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
3109
|
+
let inString = false;
|
|
3110
|
+
let escape = false;
|
|
3111
|
+
while (i < input.length) {
|
|
3112
|
+
const c = input[i];
|
|
3113
|
+
if (inString) {
|
|
3114
|
+
out += c;
|
|
3115
|
+
if (escape) escape = false;
|
|
3116
|
+
else if (c === "\\") escape = true;
|
|
3117
|
+
else if (c === "\"") inString = false;
|
|
4852
3118
|
i++;
|
|
4853
3119
|
continue;
|
|
4854
3120
|
}
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
let firstOld;
|
|
4859
|
-
let firstNew;
|
|
4860
|
-
while (i < ops.length && ops[i].op !== "context") {
|
|
4861
|
-
const cur = ops[i];
|
|
4862
|
-
if (cur.op === "add") {
|
|
4863
|
-
added++;
|
|
4864
|
-
if (firstNew === void 0) firstNew = cur.text;
|
|
4865
|
-
nl++;
|
|
4866
|
-
} else {
|
|
4867
|
-
removed++;
|
|
4868
|
-
if (firstOld === void 0) firstOld = cur.text;
|
|
4869
|
-
}
|
|
3121
|
+
if (c === "\"") {
|
|
3122
|
+
inString = true;
|
|
3123
|
+
out += c;
|
|
4870
3124
|
i++;
|
|
4871
|
-
}
|
|
4872
|
-
totalAdded += added;
|
|
4873
|
-
totalRemoved += removed;
|
|
4874
|
-
hunks.push({
|
|
4875
|
-
line: runStartLine,
|
|
4876
|
-
added,
|
|
4877
|
-
removed,
|
|
4878
|
-
...firstOld !== void 0 ? { firstOld } : {},
|
|
4879
|
-
...firstNew !== void 0 ? { firstNew } : {}
|
|
4880
|
-
});
|
|
4881
|
-
}
|
|
4882
|
-
return {
|
|
4883
|
-
totalAdded,
|
|
4884
|
-
totalRemoved,
|
|
4885
|
-
hunks
|
|
4886
|
-
};
|
|
4887
|
-
}
|
|
4888
|
-
function previewEditPayload(payload, priorContent, contextLines = 3) {
|
|
4889
|
-
const resolution = [];
|
|
4890
|
-
const resolvedHunks = [];
|
|
4891
|
-
const perHunkDiff = [];
|
|
4892
|
-
let running = priorContent;
|
|
4893
|
-
for (const hunk of payload.hunks) {
|
|
4894
|
-
if (hunk.oldString === "" || hunk.oldString === running) {
|
|
4895
|
-
resolution.push({
|
|
4896
|
-
resolved: true,
|
|
4897
|
-
via: "exact",
|
|
4898
|
-
occurrences: 1
|
|
4899
|
-
});
|
|
4900
|
-
resolvedHunks.push(hunk);
|
|
4901
|
-
perHunkDiff.push(buildContextualDiff({
|
|
4902
|
-
...payload,
|
|
4903
|
-
hunks: [hunk]
|
|
4904
|
-
}, running, contextLines));
|
|
4905
|
-
running = hunk.newString;
|
|
4906
3125
|
continue;
|
|
4907
3126
|
}
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
resolution.push({ resolved: false });
|
|
4911
|
-
resolvedHunks.push(hunk);
|
|
4912
|
-
perHunkDiff.push("");
|
|
3127
|
+
if (c === "/" && input[i + 1] === "/") {
|
|
3128
|
+
while (i < input.length && input[i] !== "\n") i++;
|
|
4913
3129
|
continue;
|
|
4914
3130
|
}
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
...hunk.replaceAll ? { replaceAll: true } : {}
|
|
4921
|
-
};
|
|
4922
|
-
resolution.push({
|
|
4923
|
-
resolved: !ambiguous,
|
|
4924
|
-
via: match.via,
|
|
4925
|
-
occurrences: match.occurrences,
|
|
4926
|
-
...ambiguous ? { ambiguous: true } : {}
|
|
4927
|
-
});
|
|
4928
|
-
resolvedHunks.push(resolvedHunk);
|
|
4929
|
-
perHunkDiff.push(ambiguous ? "" : buildContextualDiff({
|
|
4930
|
-
...payload,
|
|
4931
|
-
hunks: [resolvedHunk]
|
|
4932
|
-
}, running, contextLines));
|
|
4933
|
-
if (!ambiguous) running = hunk.replaceAll ? running.replaceAll(match.actual, styledNew) : running.replace(match.actual, styledNew);
|
|
4934
|
-
}
|
|
4935
|
-
const resolvedPayload = {
|
|
4936
|
-
...payload,
|
|
4937
|
-
hunks: resolvedHunks
|
|
4938
|
-
};
|
|
4939
|
-
const applicableHunks = resolvedHunks.filter((_, i) => resolution[i].resolved);
|
|
4940
|
-
return {
|
|
4941
|
-
diffText: applicableHunks.length === 0 ? "" : buildContextualDiff({
|
|
4942
|
-
...payload,
|
|
4943
|
-
hunks: applicableHunks
|
|
4944
|
-
}, priorContent, contextLines),
|
|
4945
|
-
resolution,
|
|
4946
|
-
perHunkDiff,
|
|
4947
|
-
resolvedPayload
|
|
4948
|
-
};
|
|
4949
|
-
}
|
|
4950
|
-
function buildUnifiedDiff(payload) {
|
|
4951
|
-
const parts = [];
|
|
4952
|
-
const isNewFile = payload.tool === "write_file" && payload.hunks[0]?.oldString === "";
|
|
4953
|
-
parts.push(isNewFile ? `--- /dev/null` : `--- a/${payload.path}`);
|
|
4954
|
-
parts.push(`+++ b/${payload.path}`);
|
|
4955
|
-
for (const hunk of payload.hunks) {
|
|
4956
|
-
const lines = computeLineDiff(hunk.oldString, hunk.newString);
|
|
4957
|
-
const oldCount = lines.filter((l) => l.op !== "add").length;
|
|
4958
|
-
const newCount = lines.filter((l) => l.op !== "remove").length;
|
|
4959
|
-
const oldStart = oldCount === 0 ? 0 : 1;
|
|
4960
|
-
const newStart = newCount === 0 ? 0 : 1;
|
|
4961
|
-
parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
4962
|
-
for (const line of lines) {
|
|
4963
|
-
const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
|
|
4964
|
-
parts.push(`${prefix}${line.text}`);
|
|
3131
|
+
if (c === "/" && input[i + 1] === "*") {
|
|
3132
|
+
i += 2;
|
|
3133
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
|
|
3134
|
+
i += 2;
|
|
3135
|
+
continue;
|
|
4965
3136
|
}
|
|
3137
|
+
out += c;
|
|
3138
|
+
i++;
|
|
4966
3139
|
}
|
|
4967
|
-
return
|
|
4968
|
-
}
|
|
4969
|
-
const FILETYPE_BY_EXT = {
|
|
4970
|
-
ts: "typescript",
|
|
4971
|
-
mts: "typescript",
|
|
4972
|
-
cts: "typescript",
|
|
4973
|
-
tsx: "tsx",
|
|
4974
|
-
js: "javascript",
|
|
4975
|
-
mjs: "javascript",
|
|
4976
|
-
cjs: "javascript",
|
|
4977
|
-
jsx: "jsx",
|
|
4978
|
-
py: "python",
|
|
4979
|
-
pyi: "python",
|
|
4980
|
-
rs: "rust",
|
|
4981
|
-
go: "go",
|
|
4982
|
-
json: "json",
|
|
4983
|
-
jsonc: "json",
|
|
4984
|
-
sh: "bash",
|
|
4985
|
-
bash: "bash",
|
|
4986
|
-
zsh: "bash",
|
|
4987
|
-
yaml: "yaml",
|
|
4988
|
-
yml: "yaml",
|
|
4989
|
-
html: "html",
|
|
4990
|
-
htm: "html",
|
|
4991
|
-
css: "css",
|
|
4992
|
-
md: "markdown",
|
|
4993
|
-
markdown: "markdown"
|
|
4994
|
-
};
|
|
4995
|
-
function filetypeFromPath(path) {
|
|
4996
|
-
const cleaned = path.split(/[?#]/, 1)[0];
|
|
4997
|
-
const lastDot = cleaned.lastIndexOf(".");
|
|
4998
|
-
if (lastDot === -1 || lastDot === cleaned.length - 1) return void 0;
|
|
4999
|
-
return FILETYPE_BY_EXT[cleaned.slice(lastDot + 1).toLowerCase()];
|
|
3140
|
+
return out;
|
|
5000
3141
|
}
|
|
5001
3142
|
//#endregion
|
|
5002
3143
|
//#region src/chat/store.ts
|
|
@@ -5311,7 +3452,21 @@ function eventsFromTurns(turns, runs = []) {
|
|
|
5311
3452
|
callId: block.id,
|
|
5312
3453
|
...tag
|
|
5313
3454
|
});
|
|
5314
|
-
}
|
|
3455
|
+
} else if (block.type === "server_tool_use") events.push({
|
|
3456
|
+
kind: "tool",
|
|
3457
|
+
text: toolCallPreview(block.name, block.input),
|
|
3458
|
+
tool: block.name,
|
|
3459
|
+
input: block.input,
|
|
3460
|
+
callId: block.id,
|
|
3461
|
+
...tag
|
|
3462
|
+
});
|
|
3463
|
+
else if (block.type === "server_tool_result") events.push({
|
|
3464
|
+
kind: "tool-result",
|
|
3465
|
+
text: serverToolResultSummary(block.content),
|
|
3466
|
+
tool: block.toolName,
|
|
3467
|
+
callId: block.toolUseId,
|
|
3468
|
+
...tag
|
|
3469
|
+
});
|
|
5315
3470
|
lastDepthAtEmission = depth;
|
|
5316
3471
|
}
|
|
5317
3472
|
}
|
|
@@ -5402,6 +3557,28 @@ function toolCallPreview(name, input) {
|
|
|
5402
3557
|
return args && args !== "{}" ? `${name}(${args})` : name;
|
|
5403
3558
|
}
|
|
5404
3559
|
/**
|
|
3560
|
+
* One-line display summary for a server-tool-result block (currently only
|
|
3561
|
+
* Anthropic's `web_search_tool_result`). `content` is either an error object
|
|
3562
|
+
* (`{ error_code: '...' }`) or an array of result entries — we count the
|
|
3563
|
+
* latter and surface the code for the former. Kept terse on purpose; richer
|
|
3564
|
+
* formatting (titles + URLs per hit) is a separate UI pass.
|
|
3565
|
+
*
|
|
3566
|
+
* Falls back to `(opaque result)` for unrecognized shapes rather than an
|
|
3567
|
+
* empty string — an empty `text` field would render as a blank row in the
|
|
3568
|
+
* transcript with no signal that anything happened.
|
|
3569
|
+
*/
|
|
3570
|
+
function serverToolResultSummary(content) {
|
|
3571
|
+
if (Array.isArray(content)) {
|
|
3572
|
+
const n = content.length;
|
|
3573
|
+
return n === 1 ? "1 result" : `${n} results`;
|
|
3574
|
+
}
|
|
3575
|
+
if (content && typeof content === "object") {
|
|
3576
|
+
const code = content.error_code;
|
|
3577
|
+
if (typeof code === "string") return `error: ${code}`;
|
|
3578
|
+
}
|
|
3579
|
+
return "(opaque result)";
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
5405
3582
|
* Update the trailing `'tool'` event matching `callId` so its
|
|
5406
3583
|
* `edit.outcomes` reflects the canonical merged outcomes the chat
|
|
5407
3584
|
* layer's `tool:transform` hook just computed. Without this, the live
|
|
@@ -5452,63 +3629,6 @@ function toolResultText(output) {
|
|
|
5452
3629
|
function stripSpawnTokensLine(text) {
|
|
5453
3630
|
return text.replace(/(^\[sub-agent [^\n]+\n)Tokens:[^\n]*\n?/gm, "$1");
|
|
5454
3631
|
}
|
|
5455
|
-
/** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
|
|
5456
|
-
const EDIT_TOOL_NAMES = new Set([
|
|
5457
|
-
"edit",
|
|
5458
|
-
"multi_edit",
|
|
5459
|
-
"write_file"
|
|
5460
|
-
]);
|
|
5461
|
-
/**
|
|
5462
|
-
* Recognize a tool-result body as carrying NON-success information so the
|
|
5463
|
-
* renderer doesn't suppress it under `showEditDiffs`. Three categories:
|
|
5464
|
-
*
|
|
5465
|
-
* - `edit` → "Edit error: …"
|
|
5466
|
-
* - `write_file` permission errors wrapped by the loop → "Tool failed: …"
|
|
5467
|
-
* - `multi_edit` → legacy single-line error `multi_edit error: …`, OR
|
|
5468
|
-
* a result carrying an `<edit-outcomes>…</edit-outcomes>` annotation
|
|
5469
|
-
* block. The TUI only appends the annotation when at least one hunk
|
|
5470
|
-
* was NOT applied, so its mere presence is the signal — the result
|
|
5471
|
-
* body needs to stay visible next to the diff so the user can read
|
|
5472
|
-
* denial / skip / failure reasons longer than the per-hunk badge.
|
|
5473
|
-
* - Fully-denied gate emit (`[fully denied] <edit-outcomes>…`) likewise
|
|
5474
|
-
* stays visible.
|
|
5475
|
-
*
|
|
5476
|
-
* Exported for unit-testability of the visibility matrix.
|
|
5477
|
-
*/
|
|
5478
|
-
function isEditErrorResult(text) {
|
|
5479
|
-
if (text.startsWith("Edit error:")) return true;
|
|
5480
|
-
if (text.startsWith("Tool failed:")) return true;
|
|
5481
|
-
if (text.startsWith("multi_edit error:")) return true;
|
|
5482
|
-
if (text.startsWith("[fully denied]")) return true;
|
|
5483
|
-
if (text.includes("\n<edit-outcomes>\n") || text.startsWith("<edit-outcomes>\n")) return true;
|
|
5484
|
-
return false;
|
|
5485
|
-
}
|
|
5486
|
-
/**
|
|
5487
|
-
* Per-event visibility — filters honor user toggles and the
|
|
5488
|
-
* `hideSubagentOutput` setting. When subagent output is hidden:
|
|
5489
|
-
* - Child-agent events are filtered down to the `spawn-start` /
|
|
5490
|
-
* `spawn-end` markers so the user still sees "🌱 working… 🌳 done".
|
|
5491
|
-
* - The parent's `tool-result` for `spawn` is hidden too. Its body
|
|
5492
|
-
* duplicates `spawn-end`'s stats line *and* the parent's next
|
|
5493
|
-
* markdown turn; showing it again produces an extra
|
|
5494
|
-
* `┃ [sub-agent child-1] Completed …` block users just want gone.
|
|
5495
|
-
*
|
|
5496
|
-
* Renderer-agnostic — returns plain `boolean` so TUI / GUI consumers
|
|
5497
|
-
* can filter events identically.
|
|
5498
|
-
*/
|
|
5499
|
-
function isVisible(event, settings) {
|
|
5500
|
-
if (settings.hideSubagentOutput) {
|
|
5501
|
-
if ((event.depth ?? 0) > 0) return event.kind === "spawn-start" || event.kind === "spawn-end";
|
|
5502
|
-
if (event.kind === "tool-result" && event.tool === "spawn") return false;
|
|
5503
|
-
}
|
|
5504
|
-
if (settings.showEditDiffs && event.kind === "tool-result" && event.tool && EDIT_TOOL_NAMES.has(event.tool) && !isEditErrorResult(event.text)) return false;
|
|
5505
|
-
switch (event.kind) {
|
|
5506
|
-
case "thinking": return settings.showThinking;
|
|
5507
|
-
case "tool": return settings.toolCallDisplay !== "hidden";
|
|
5508
|
-
case "tool-result": return settings.showToolResults;
|
|
5509
|
-
default: return true;
|
|
5510
|
-
}
|
|
5511
|
-
}
|
|
5512
3632
|
/**
|
|
5513
3633
|
* Default top-margin per kind (in rows). Spacing intent:
|
|
5514
3634
|
* - `info` / `markdown` / `tool` / `error` / `spawn-start` open a new
|
|
@@ -5564,114 +3684,6 @@ function marginTopFor(event, previous) {
|
|
|
5564
3684
|
if (event.kind === "task-notification" && previous?.kind === "task-notification") return 0;
|
|
5565
3685
|
return MARGIN_TOP[event.kind] ?? 0;
|
|
5566
3686
|
}
|
|
5567
|
-
/**
|
|
5568
|
-
* Build the `resultTurnId → owningAssistantTurnId` map used by the select-
|
|
5569
|
-
* turn mode to coalesce a tool-call's surrounding turns into ONE navigation
|
|
5570
|
-
* stop.
|
|
5571
|
-
*
|
|
5572
|
-
* Protocol shape: every `tool_call` block in an assistant turn is closed by
|
|
5573
|
-
* a matching `tool_result` block in the *next* user turn (the agent loop's
|
|
5574
|
-
* history validator depends on this). When the next user turn's only events
|
|
5575
|
-
* are `tool-result`s — i.e. it's pure plumbing for the prior assistant
|
|
5576
|
-
* turn — we map it back to that assistant turn here. The select-turn nav
|
|
5577
|
-
* index ({@link selectableTurnIds}) skips owned turns, and the renderer's
|
|
5578
|
-
* highlight gate ({@link isTurnHighlighted}) extends the selection accent
|
|
5579
|
-
* from the assistant turn to the events of any turn it owns. Net effect:
|
|
5580
|
-
*
|
|
5581
|
-
* - Navigation never lands the cursor on a result-only turn whose own
|
|
5582
|
-
* events may be hidden by `showToolResults: false` — the cursor
|
|
5583
|
-
* wouldn't be visible.
|
|
5584
|
-
* - Selecting an assistant turn highlights the call AND its result as
|
|
5585
|
-
* one unit, matching the user's mental model of "one message".
|
|
5586
|
-
*
|
|
5587
|
-
* Owner-lookup is conservative: result-only turns with no matching prior
|
|
5588
|
-
* assistant turn (orphaned — usually because the parent was deleted)
|
|
5589
|
-
* stay selectable so the user can act on them via the turn-details modal.
|
|
5590
|
-
*
|
|
5591
|
-
* Subagent (`childId` set) events are ignored — they live in a separate
|
|
5592
|
-
* conversation tree.
|
|
5593
|
-
*/
|
|
5594
|
-
function turnSelectionOwnership(events) {
|
|
5595
|
-
const orderedTurnIds = [];
|
|
5596
|
-
const eventKindsByTurn = /* @__PURE__ */ new Map();
|
|
5597
|
-
for (const e of events) {
|
|
5598
|
-
if (!e.turnId) continue;
|
|
5599
|
-
if (e.childId) continue;
|
|
5600
|
-
if (!eventKindsByTurn.has(e.turnId)) {
|
|
5601
|
-
orderedTurnIds.push(e.turnId);
|
|
5602
|
-
eventKindsByTurn.set(e.turnId, []);
|
|
5603
|
-
}
|
|
5604
|
-
eventKindsByTurn.get(e.turnId).push(e.kind);
|
|
5605
|
-
}
|
|
5606
|
-
const ownership = /* @__PURE__ */ new Map();
|
|
5607
|
-
let lastToolEmitterTurnId = null;
|
|
5608
|
-
for (const tid of orderedTurnIds) {
|
|
5609
|
-
const kinds = eventKindsByTurn.get(tid);
|
|
5610
|
-
if (kinds.length > 0 && kinds.every((k) => k === "tool-result")) {
|
|
5611
|
-
if (lastToolEmitterTurnId) ownership.set(tid, lastToolEmitterTurnId);
|
|
5612
|
-
continue;
|
|
5613
|
-
}
|
|
5614
|
-
if (kinds.includes("tool")) lastToolEmitterTurnId = tid;
|
|
5615
|
-
}
|
|
5616
|
-
return ownership;
|
|
5617
|
-
}
|
|
5618
|
-
/**
|
|
5619
|
-
* Render-time check: should `event` paint with the selection accent?
|
|
5620
|
-
*
|
|
5621
|
-
* `true` when the event's own turn is selected, OR when the selected turn
|
|
5622
|
-
* `owns` the event's turn via {@link turnSelectionOwnership} (the call and
|
|
5623
|
-
* its tool-result rows highlight together). `false` when nothing is
|
|
5624
|
-
* selected or the relationship doesn't apply.
|
|
5625
|
-
*
|
|
5626
|
-
* Pure. Renderer-agnostic — the TUI's `<Transcript>` uses it; a GUI's
|
|
5627
|
-
* equivalent walks the same rule.
|
|
5628
|
-
*/
|
|
5629
|
-
function isTurnHighlighted(event, selectedTurnId, ownership) {
|
|
5630
|
-
if (selectedTurnId === null || !event.turnId) return false;
|
|
5631
|
-
if (event.turnId === selectedTurnId) return true;
|
|
5632
|
-
return ownership.get(event.turnId) === selectedTurnId;
|
|
5633
|
-
}
|
|
5634
|
-
/**
|
|
5635
|
-
* Deduplicated, in-order list of **parent-conversation** turn ids that appear
|
|
5636
|
-
* in a rendered transcript — the navigation index for the TUI's select-turn
|
|
5637
|
-
* mode. Three classes of turns are deliberately skipped:
|
|
5638
|
-
*
|
|
5639
|
-
* - **Subagent turns** (`childId` set). Nested execution detail; the
|
|
5640
|
-
* user's mental model of a "message" is the conversational exchange,
|
|
5641
|
-
* not each spawn turn. Also filtered out by `isVisible` under
|
|
5642
|
-
* `hideSubagentOutput: true` — selecting them would highlight nothing.
|
|
5643
|
-
* - **Result-only turns** — see {@link turnSelectionOwnership}. These get
|
|
5644
|
-
* coalesced into the assistant turn that emitted their tool_calls.
|
|
5645
|
-
* - **Settings-hidden turns** (when `settings` is supplied). A turn whose
|
|
5646
|
-
* every event fails {@link isVisible} would render no rows — landing
|
|
5647
|
-
* the cursor there hides it from the user entirely. The check is opt-
|
|
5648
|
-
* in so SDK callers without a Settings object keep the legacy
|
|
5649
|
-
* "everything visible" behavior.
|
|
5650
|
-
*
|
|
5651
|
-
* Synthetic events (separator, spawn-start, spawn-end) have no `turnId` and
|
|
5652
|
-
* are skipped naturally.
|
|
5653
|
-
*/
|
|
5654
|
-
function selectableTurnIds(events, settings) {
|
|
5655
|
-
const ownership = turnSelectionOwnership(events);
|
|
5656
|
-
const visibleCount = settings ? /* @__PURE__ */ new Map() : null;
|
|
5657
|
-
if (settings && visibleCount) for (const e of events) {
|
|
5658
|
-
if (!e.turnId || e.childId) continue;
|
|
5659
|
-
if (!isVisible(e, settings)) continue;
|
|
5660
|
-
visibleCount.set(e.turnId, (visibleCount.get(e.turnId) ?? 0) + 1);
|
|
5661
|
-
}
|
|
5662
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5663
|
-
const ordered = [];
|
|
5664
|
-
for (const e of events) {
|
|
5665
|
-
if (!e.turnId) continue;
|
|
5666
|
-
if (e.childId) continue;
|
|
5667
|
-
if (seen.has(e.turnId)) continue;
|
|
5668
|
-
if (ownership.has(e.turnId)) continue;
|
|
5669
|
-
if (visibleCount && (visibleCount.get(e.turnId) ?? 0) === 0) continue;
|
|
5670
|
-
seen.add(e.turnId);
|
|
5671
|
-
ordered.push(e.turnId);
|
|
5672
|
-
}
|
|
5673
|
-
return ordered;
|
|
5674
|
-
}
|
|
5675
3687
|
/** Effective context size of the most recent assistant turn — drives the footer indicator. */
|
|
5676
3688
|
/**
|
|
5677
3689
|
* Walk from the end of `turns` and return the cache-aware input-token total
|
|
@@ -6020,10 +4032,15 @@ function pickInitial(auth, providers, state) {
|
|
|
6020
4032
|
const descriptor = providers[auth.key];
|
|
6021
4033
|
if (!descriptor) return null;
|
|
6022
4034
|
const model = state.lastModelByProvider?.[auth.key] ?? descriptor.defaultModel ?? safeFactoryDefault(descriptor);
|
|
6023
|
-
|
|
4035
|
+
if (!model) return null;
|
|
4036
|
+
const effort = modelSupportsReasoning(descriptor, model) ? state.lastEffortByModel?.[model] ?? "medium" : void 0;
|
|
4037
|
+
const modelOptions = restoreModelOptions(descriptor, model, state.lastModelOptionsByModel);
|
|
4038
|
+
return {
|
|
6024
4039
|
provider: auth,
|
|
6025
|
-
model
|
|
6026
|
-
|
|
4040
|
+
model,
|
|
4041
|
+
...effort ? { effort } : {},
|
|
4042
|
+
...modelOptions ? { modelOptions } : {}
|
|
4043
|
+
};
|
|
6027
4044
|
}
|
|
6028
4045
|
function safeFactoryDefault(descriptor) {
|
|
6029
4046
|
try {
|
|
@@ -7930,7 +5947,7 @@ function toEntries(paths, source, maxFiles) {
|
|
|
7930
5947
|
* effort picker as a discoverable, in-place affordance.
|
|
7931
5948
|
*/
|
|
7932
5949
|
function buildHints(options) {
|
|
7933
|
-
const { screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, uiMode, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor, updateHint } = options;
|
|
5950
|
+
const { screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, uiMode, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, modelOptionsLabel, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor, updateHint } = options;
|
|
7934
5951
|
if (pending) return [
|
|
7935
5952
|
{
|
|
7936
5953
|
key: "↑↓",
|
|
@@ -8021,14 +6038,15 @@ function buildHints(options) {
|
|
|
8021
6038
|
label: currentSession ? "back" : "exit"
|
|
8022
6039
|
}
|
|
8023
6040
|
];
|
|
6041
|
+
const chordLabel = [effortLabel, modelOptionsLabel].filter(Boolean).join(" ") || null;
|
|
8024
6042
|
const modelHint = modelLabel ? {
|
|
8025
6043
|
key: keybindings.openModelPicker,
|
|
8026
6044
|
label: modelLabel,
|
|
8027
6045
|
labelColor: modelColor,
|
|
8028
|
-
...
|
|
6046
|
+
...chordLabel ? { extra: {
|
|
8029
6047
|
key: shortChord(keybindings.openEffortPicker),
|
|
8030
6048
|
keyColor: effortKeyColor,
|
|
8031
|
-
label:
|
|
6049
|
+
label: chordLabel,
|
|
8032
6050
|
labelColor: effortColor
|
|
8033
6051
|
} } : {}
|
|
8034
6052
|
} : null;
|
|
@@ -8043,6 +6061,10 @@ function buildHints(options) {
|
|
|
8043
6061
|
label: inFlightToolCount === 1 ? "cancel task" : `cancel task (${inFlightToolCount})`
|
|
8044
6062
|
} : null;
|
|
8045
6063
|
if (uiMode === "minimal") return [
|
|
6064
|
+
{
|
|
6065
|
+
key: keybindings.openContextPanel,
|
|
6066
|
+
label: "context"
|
|
6067
|
+
},
|
|
8046
6068
|
...hasMultipleAgents ? [{
|
|
8047
6069
|
key: keybindings.cycleAgent,
|
|
8048
6070
|
label: agentLabel,
|
|
@@ -8055,6 +6077,10 @@ function buildHints(options) {
|
|
|
8055
6077
|
}
|
|
8056
6078
|
];
|
|
8057
6079
|
return [
|
|
6080
|
+
{
|
|
6081
|
+
key: keybindings.openContextPanel,
|
|
6082
|
+
label: "context"
|
|
6083
|
+
},
|
|
8058
6084
|
...hasMultipleAgents ? [{
|
|
8059
6085
|
key: keybindings.cycleAgent,
|
|
8060
6086
|
label: agentLabel,
|
|
@@ -9575,79 +7601,6 @@ function filterByEnabled(discovered, enabled) {
|
|
|
9575
7601
|
return discovered.filter((d) => allow.has(d.config.name));
|
|
9576
7602
|
}
|
|
9577
7603
|
//#endregion
|
|
9578
|
-
//#region src/chat/model-catalog.ts
|
|
9579
|
-
/**
|
|
9580
|
-
* Build the unified catalog from a list of available providers.
|
|
9581
|
-
*
|
|
9582
|
-
* Provider order is preserved (callers typically pass the picker order
|
|
9583
|
-
* — alphabetical, auth-detection order, etc.); model order inside each
|
|
9584
|
-
* provider matches whatever `modelsFor` returns. The current selection
|
|
9585
|
-
* (when set) is bubbled to the top of its provider's section so it
|
|
9586
|
-
* shows first without disturbing relative ordering elsewhere.
|
|
9587
|
-
*
|
|
9588
|
-
* `modelsFor` is injected (not imported from `./providers`) so the same
|
|
9589
|
-
* helper works with hosts that supply their own model resolver via
|
|
9590
|
-
* `ResolvedConfig.modelsFor`.
|
|
9591
|
-
*/
|
|
9592
|
-
function buildModelCatalog(opts) {
|
|
9593
|
-
const entries = [];
|
|
9594
|
-
for (const provider of opts.providers) {
|
|
9595
|
-
const models = opts.modelsFor(provider.key);
|
|
9596
|
-
if (models.length === 0) continue;
|
|
9597
|
-
let ordered = models;
|
|
9598
|
-
if (opts.current?.providerKey === provider.key) {
|
|
9599
|
-
const idx = models.findIndex((m) => m.id === opts.current?.modelId);
|
|
9600
|
-
if (idx > 0) {
|
|
9601
|
-
const next = models.slice();
|
|
9602
|
-
const [active] = next.splice(idx, 1);
|
|
9603
|
-
next.unshift(active);
|
|
9604
|
-
ordered = next;
|
|
9605
|
-
}
|
|
9606
|
-
}
|
|
9607
|
-
for (const model of ordered) entries.push({
|
|
9608
|
-
providerKey: provider.key,
|
|
9609
|
-
providerLabel: provider.label,
|
|
9610
|
-
model,
|
|
9611
|
-
searchCorpus: buildSearchCorpus(provider, model)
|
|
9612
|
-
});
|
|
9613
|
-
}
|
|
9614
|
-
return entries;
|
|
9615
|
-
}
|
|
9616
|
-
/**
|
|
9617
|
-
* Filter `catalog` by a user query. Empty / whitespace-only queries
|
|
9618
|
-
* pass everything through unchanged (`O(1)` short-circuit). Multi-term
|
|
9619
|
-
* queries (space-separated) require EVERY term to appear somewhere in
|
|
9620
|
-
* the entry's search corpus — so `"claude opus"` matches `claude-opus-4`
|
|
9621
|
-
* regardless of how the words are interleaved with provider names.
|
|
9622
|
-
*
|
|
9623
|
-
* Match is case-insensitive (the corpus is pre-lowercased; the query
|
|
9624
|
-
* is lowercased once per call).
|
|
9625
|
-
*/
|
|
9626
|
-
function filterModelCatalog(catalog, query) {
|
|
9627
|
-
const trimmed = query.trim().toLowerCase();
|
|
9628
|
-
if (!trimmed) return catalog.slice();
|
|
9629
|
-
const terms = trimmed.split(/\s+/);
|
|
9630
|
-
return catalog.filter((entry) => terms.every((t) => entry.searchCorpus.includes(t)));
|
|
9631
|
-
}
|
|
9632
|
-
/**
|
|
9633
|
-
* Find a catalog entry's index by its `{providerKey, modelId}` tuple.
|
|
9634
|
-
* Returns `-1` when not present. Useful when re-rendering the picker
|
|
9635
|
-
* (a query just narrowed the list, where did the selection land?).
|
|
9636
|
-
*/
|
|
9637
|
-
function indexOfEntry(catalog, target) {
|
|
9638
|
-
if (!target) return -1;
|
|
9639
|
-
return catalog.findIndex((e) => e.providerKey === target.providerKey && e.model.id === target.modelId);
|
|
9640
|
-
}
|
|
9641
|
-
function buildSearchCorpus(provider, model) {
|
|
9642
|
-
return [
|
|
9643
|
-
provider.key,
|
|
9644
|
-
provider.label,
|
|
9645
|
-
model.id,
|
|
9646
|
-
model.name ?? "",
|
|
9647
|
-
model.provider ?? ""
|
|
9648
|
-
].join(" ").toLowerCase();
|
|
9649
|
-
}
|
|
9650
|
-
//#endregion
|
|
9651
7604
|
//#region src/chat/oauth.ts
|
|
9652
7605
|
function supportsOAuth(descriptor) {
|
|
9653
7606
|
return descriptor.oauthProvider !== void 0;
|
|
@@ -9836,59 +7789,6 @@ function formatPathForCwd(projectRelativePath, projectRoot, cwd) {
|
|
|
9836
7789
|
return sep === "/" ? rel : rel.split(sep).join(posix.sep);
|
|
9837
7790
|
}
|
|
9838
7791
|
//#endregion
|
|
9839
|
-
//#region src/chat/prompt-segments.ts
|
|
9840
|
-
/**
|
|
9841
|
-
* Split a prompt buffer into word-sized atomic segments suitable for a
|
|
9842
|
-
* flex-row + flex-wrap renderer (TUI) or a `display: inline` flow with
|
|
9843
|
-
* inline-block chips (GUI). Each chip becomes one segment (atomic —
|
|
9844
|
-
* never broken across rows); each plain run is split into "word +
|
|
9845
|
-
* trailing space" units so wraps land at clean word boundaries.
|
|
9846
|
-
*
|
|
9847
|
-
* Robust to:
|
|
9848
|
-
* - Overlapping refs — sorted by start; later refs that overlap are
|
|
9849
|
-
* dropped via the first-wins rule.
|
|
9850
|
-
* - Out-of-bounds refs — dropped entirely when `end > text.length` or
|
|
9851
|
-
* `start >= text.length`. Partial clipping would silently truncate
|
|
9852
|
-
* a chip's label; the caller is in a better position to surface the
|
|
9853
|
-
* mismatch (typically a stale `refs` array referencing a previous text).
|
|
9854
|
-
* - Whitespace-only plain runs — emitted as their own plain segment
|
|
9855
|
-
* so chip-adjacent-to-chip cases keep the original spacing.
|
|
9856
|
-
*
|
|
9857
|
-
* Word splitter rationale: `\S+\s*` keeps trailing whitespace attached
|
|
9858
|
-
* to its preceding word so wrap boundaries land between words (cleanly).
|
|
9859
|
-
* A leading-whitespace-only segment is captured by `\s+` so we don't
|
|
9860
|
-
* drop it entirely when the plain run starts with a space.
|
|
9861
|
-
*/
|
|
9862
|
-
function splitPromptSegments(text, refs) {
|
|
9863
|
-
const sorted = [...refs].filter((r) => r.end > r.start && r.start < text.length && r.end <= text.length).sort((a, b) => a.start - b.start);
|
|
9864
|
-
const out = [];
|
|
9865
|
-
let cursor = 0;
|
|
9866
|
-
for (const ref of sorted) {
|
|
9867
|
-
if (ref.start < cursor) continue;
|
|
9868
|
-
if (ref.start > cursor) {
|
|
9869
|
-
const matches = text.slice(cursor, ref.start).match(/\S+\s*|\s+/g) ?? [];
|
|
9870
|
-
for (const m of matches) out.push({
|
|
9871
|
-
kind: "plain",
|
|
9872
|
-
text: m
|
|
9873
|
-
});
|
|
9874
|
-
}
|
|
9875
|
-
out.push({
|
|
9876
|
-
kind: "chip",
|
|
9877
|
-
text: text.slice(ref.start, ref.end),
|
|
9878
|
-
providerId: ref.providerId
|
|
9879
|
-
});
|
|
9880
|
-
cursor = ref.end;
|
|
9881
|
-
}
|
|
9882
|
-
if (cursor < text.length) {
|
|
9883
|
-
const matches = text.slice(cursor).match(/\S+\s*|\s+/g) ?? [];
|
|
9884
|
-
for (const m of matches) out.push({
|
|
9885
|
-
kind: "plain",
|
|
9886
|
-
text: m
|
|
9887
|
-
});
|
|
9888
|
-
}
|
|
9889
|
-
return out;
|
|
9890
|
-
}
|
|
9891
|
-
//#endregion
|
|
9892
7792
|
//#region src/chat/shell-parse.ts
|
|
9893
7793
|
/**
|
|
9894
7794
|
* Lightweight shell command parser for safelist enforcement.
|
|
@@ -10797,9 +8697,6 @@ function appendThinkingLines(prev, delta, owner, depth, turnId) {
|
|
|
10797
8697
|
}, owner, depth, turnId));
|
|
10798
8698
|
return result;
|
|
10799
8699
|
}
|
|
10800
|
-
function ownerOf(evt) {
|
|
10801
|
-
return evt.childId ?? PARENT_OWNER;
|
|
10802
|
-
}
|
|
10803
8700
|
/**
|
|
10804
8701
|
* Stamp owner (parent vs subagent) + depth + optional `turnId` onto a
|
|
10805
8702
|
* freshly-minted event so consumers can identify the producer and the
|
|
@@ -10818,53 +8715,6 @@ function tagEvent(evt, owner, depth, turnId) {
|
|
|
10818
8715
|
depth
|
|
10819
8716
|
};
|
|
10820
8717
|
}
|
|
10821
|
-
/** Flip any trailing streaming markdown blocks (any owner) to finalized. */
|
|
10822
|
-
function finalizeStreamingMarkdown(events) {
|
|
10823
|
-
let changed = false;
|
|
10824
|
-
const next = events.map((e) => {
|
|
10825
|
-
if (e.kind === "markdown" && e.streaming) {
|
|
10826
|
-
changed = true;
|
|
10827
|
-
return {
|
|
10828
|
-
...e,
|
|
10829
|
-
streaming: false
|
|
10830
|
-
};
|
|
10831
|
-
}
|
|
10832
|
-
return e;
|
|
10833
|
-
});
|
|
10834
|
-
return changed ? next : events;
|
|
10835
|
-
}
|
|
10836
|
-
/** Flip the trailing streaming markdown block for one specific owner. */
|
|
10837
|
-
function finalizeStreamingMarkdownForOwner(events, owner) {
|
|
10838
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
10839
|
-
const e = events[i];
|
|
10840
|
-
if (e.kind !== "markdown") continue;
|
|
10841
|
-
if (!e.streaming) continue;
|
|
10842
|
-
if (ownerOf(e) !== owner) continue;
|
|
10843
|
-
const next = events.slice();
|
|
10844
|
-
next[i] = {
|
|
10845
|
-
...e,
|
|
10846
|
-
streaming: false
|
|
10847
|
-
};
|
|
10848
|
-
return next;
|
|
10849
|
-
}
|
|
10850
|
-
return events;
|
|
10851
|
-
}
|
|
10852
|
-
/**
|
|
10853
|
-
* Effective context size for a single turn.
|
|
10854
|
-
*
|
|
10855
|
-
* `usage.input` is misleading on its own when prompt caching is active: providers
|
|
10856
|
-
* (Anthropic, OpenRouter→Anthropic, Gemini) report `input` as the *new uncached*
|
|
10857
|
-
* tokens only — the cached prefix shows up in `cacheRead`, and newly-cached
|
|
10858
|
-
* tokens in `cacheCreation`. The model still saw all three buckets, so the real
|
|
10859
|
-
* context-window utilization is their sum.
|
|
10860
|
-
*
|
|
10861
|
-
* Non-caching providers leave `cacheRead`/`cacheCreation` undefined, so this
|
|
10862
|
-
* collapses to plain `input` for them.
|
|
10863
|
-
*/
|
|
10864
|
-
function turnContextSize(usage) {
|
|
10865
|
-
if (!usage) return 0;
|
|
10866
|
-
return (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheCreation ?? 0);
|
|
10867
|
-
}
|
|
10868
8718
|
/**
|
|
10869
8719
|
* Target chars-per-second for the next smooth-mode tick given the
|
|
10870
8720
|
* current bucket backlog. Two linear segments — base→burst over the
|
|
@@ -11147,307 +8997,6 @@ function useSyntaxStyles() {
|
|
|
11147
8997
|
return useContext(ThemeContext).syntax;
|
|
11148
8998
|
}
|
|
11149
8999
|
//#endregion
|
|
11150
|
-
//#region src/chat/tool-formatters.ts
|
|
11151
|
-
const TOOL_DISPLAY = {
|
|
11152
|
-
read_file: {
|
|
11153
|
-
displayName: "Read",
|
|
11154
|
-
format: (input) => {
|
|
11155
|
-
const path = stringField(input, "path");
|
|
11156
|
-
if (!path) return null;
|
|
11157
|
-
const meta = [];
|
|
11158
|
-
const offset = numberField(input, "offset");
|
|
11159
|
-
const limit = numberField(input, "limit");
|
|
11160
|
-
if (offset !== void 0 && limit !== void 0 && limit > 0) meta.push(`L${offset}–${offset + limit - 1}`);
|
|
11161
|
-
else if (offset !== void 0) meta.push(`from L${offset}`);
|
|
11162
|
-
else if (limit !== void 0 && limit > 0) meta.push(`${limit} lines`);
|
|
11163
|
-
return {
|
|
11164
|
-
target: path,
|
|
11165
|
-
meta
|
|
11166
|
-
};
|
|
11167
|
-
}
|
|
11168
|
-
},
|
|
11169
|
-
list_files: {
|
|
11170
|
-
displayName: "List",
|
|
11171
|
-
format: (input) => {
|
|
11172
|
-
return { target: stringField(input, "path") ?? "." };
|
|
11173
|
-
}
|
|
11174
|
-
},
|
|
11175
|
-
glob: {
|
|
11176
|
-
displayName: "Glob",
|
|
11177
|
-
format: (input) => {
|
|
11178
|
-
const pattern = stringField(input, "pattern");
|
|
11179
|
-
if (!pattern) return null;
|
|
11180
|
-
const meta = [];
|
|
11181
|
-
const limit = numberField(input, "limit");
|
|
11182
|
-
if (limit !== void 0) meta.push(`limit ${limit}`);
|
|
11183
|
-
return {
|
|
11184
|
-
target: pattern,
|
|
11185
|
-
meta
|
|
11186
|
-
};
|
|
11187
|
-
}
|
|
11188
|
-
},
|
|
11189
|
-
grep: {
|
|
11190
|
-
displayName: "Grep",
|
|
11191
|
-
format: (input) => {
|
|
11192
|
-
const pattern = stringField(input, "pattern");
|
|
11193
|
-
if (!pattern) return null;
|
|
11194
|
-
const target = `/${pattern}/`;
|
|
11195
|
-
const meta = [];
|
|
11196
|
-
const path = stringField(input, "path");
|
|
11197
|
-
if (path && path !== ".") meta.push(`in ${path}`);
|
|
11198
|
-
const glob = stringField(input, "glob");
|
|
11199
|
-
if (glob) meta.push(glob);
|
|
11200
|
-
const type = stringField(input, "type");
|
|
11201
|
-
if (type) meta.push(`type:${type}`);
|
|
11202
|
-
if (input["-i"] === true) meta.push("case-insensitive");
|
|
11203
|
-
const mode = stringField(input, "output_mode");
|
|
11204
|
-
if (mode && mode !== "files_with_matches") meta.push(mode);
|
|
11205
|
-
return {
|
|
11206
|
-
target,
|
|
11207
|
-
meta
|
|
11208
|
-
};
|
|
11209
|
-
}
|
|
11210
|
-
},
|
|
11211
|
-
shell: {
|
|
11212
|
-
displayName: (input) => input?.run_in_background === true ? "Shell (background)" : "Shell",
|
|
11213
|
-
format: (input) => {
|
|
11214
|
-
const command = stringField(input, "command");
|
|
11215
|
-
if (!command) return null;
|
|
11216
|
-
const description = stringField(input, "description");
|
|
11217
|
-
const line = { target: truncate(command, 200) };
|
|
11218
|
-
if (description && description.trim() !== "") line.meta = [truncate(description, 100)];
|
|
11219
|
-
return line;
|
|
11220
|
-
}
|
|
11221
|
-
},
|
|
11222
|
-
shell_kill: {
|
|
11223
|
-
displayName: "Kill task",
|
|
11224
|
-
format: (input) => {
|
|
11225
|
-
const taskId = stringField(input, "task_id");
|
|
11226
|
-
if (!taskId) return null;
|
|
11227
|
-
return { target: taskId };
|
|
11228
|
-
}
|
|
11229
|
-
},
|
|
11230
|
-
edit: {
|
|
11231
|
-
displayName: "Edit",
|
|
11232
|
-
format: (input) => {
|
|
11233
|
-
const path = stringField(input, "path");
|
|
11234
|
-
if (!path) return null;
|
|
11235
|
-
return {
|
|
11236
|
-
target: path,
|
|
11237
|
-
meta: input.replace_all === true ? ["replace all"] : []
|
|
11238
|
-
};
|
|
11239
|
-
}
|
|
11240
|
-
},
|
|
11241
|
-
multi_edit: {
|
|
11242
|
-
displayName: "Multi-edit",
|
|
11243
|
-
format: (input) => {
|
|
11244
|
-
const path = stringField(input, "path");
|
|
11245
|
-
if (!path) return null;
|
|
11246
|
-
const edits = Array.isArray(input.edits) ? input.edits.length : 0;
|
|
11247
|
-
return {
|
|
11248
|
-
target: path,
|
|
11249
|
-
meta: edits > 0 ? [`${edits} hunk${edits === 1 ? "" : "s"}`] : []
|
|
11250
|
-
};
|
|
11251
|
-
}
|
|
11252
|
-
},
|
|
11253
|
-
write_file: {
|
|
11254
|
-
displayName: "Write",
|
|
11255
|
-
format: (input) => {
|
|
11256
|
-
const path = stringField(input, "path");
|
|
11257
|
-
if (!path) return null;
|
|
11258
|
-
const content = stringField(input, "content");
|
|
11259
|
-
const meta = [];
|
|
11260
|
-
if (content !== void 0) {
|
|
11261
|
-
const bytes = byteLengthUtf8(content);
|
|
11262
|
-
meta.push(`${formatBytes(bytes)}`);
|
|
11263
|
-
}
|
|
11264
|
-
return {
|
|
11265
|
-
target: path,
|
|
11266
|
-
meta
|
|
11267
|
-
};
|
|
11268
|
-
}
|
|
11269
|
-
},
|
|
11270
|
-
spawn: {
|
|
11271
|
-
displayName: "Agent",
|
|
11272
|
-
format: (input) => {
|
|
11273
|
-
const task = stringField(input, "task");
|
|
11274
|
-
if (!task) return null;
|
|
11275
|
-
return { target: truncate(task, 120) };
|
|
11276
|
-
}
|
|
11277
|
-
},
|
|
11278
|
-
tool_search: {
|
|
11279
|
-
displayName: "Search tools",
|
|
11280
|
-
format: (input) => {
|
|
11281
|
-
const query = stringField(input, "query");
|
|
11282
|
-
const names = Array.isArray(input.names) ? input.names.length : 0;
|
|
11283
|
-
if (query) return { target: `“${query}”` };
|
|
11284
|
-
if (names > 0) return { target: `${names} tool${names === 1 ? "" : "s"}` };
|
|
11285
|
-
return null;
|
|
11286
|
-
}
|
|
11287
|
-
},
|
|
11288
|
-
skills_use: {
|
|
11289
|
-
displayName: (input) => {
|
|
11290
|
-
return (input ? stringField(input, "mode") : void 0) === "deactivate" ? "Disable skill" : "Enable skill";
|
|
11291
|
-
},
|
|
11292
|
-
format: (input) => {
|
|
11293
|
-
const name = stringField(input, "name");
|
|
11294
|
-
if (!name) return null;
|
|
11295
|
-
return { target: name };
|
|
11296
|
-
}
|
|
11297
|
-
},
|
|
11298
|
-
skills_read: {
|
|
11299
|
-
displayName: "Read skill",
|
|
11300
|
-
format: (input) => {
|
|
11301
|
-
const name = stringField(input, "name");
|
|
11302
|
-
const path = stringField(input, "path");
|
|
11303
|
-
if (!name) return null;
|
|
11304
|
-
return { target: path ? `${name}/${path}` : name };
|
|
11305
|
-
}
|
|
11306
|
-
},
|
|
11307
|
-
skills_run_script: {
|
|
11308
|
-
displayName: "Run script",
|
|
11309
|
-
format: (input) => {
|
|
11310
|
-
const name = stringField(input, "name");
|
|
11311
|
-
const script = stringField(input, "script");
|
|
11312
|
-
if (!name || !script) return null;
|
|
11313
|
-
const meta = [`skill ${name}`];
|
|
11314
|
-
const args = Array.isArray(input.args) ? input.args : null;
|
|
11315
|
-
if (args && args.length > 0) meta.push(truncate(args.map(String).join(" "), 80));
|
|
11316
|
-
return {
|
|
11317
|
-
target: script,
|
|
11318
|
-
meta
|
|
11319
|
-
};
|
|
11320
|
-
}
|
|
11321
|
-
},
|
|
11322
|
-
todowrite: {
|
|
11323
|
-
displayName: "Todos",
|
|
11324
|
-
format: (input) => {
|
|
11325
|
-
const todos = Array.isArray(input.todos) ? input.todos : null;
|
|
11326
|
-
if (!todos) return null;
|
|
11327
|
-
const counts = {
|
|
11328
|
-
pending: 0,
|
|
11329
|
-
in_progress: 0,
|
|
11330
|
-
completed: 0,
|
|
11331
|
-
cancelled: 0
|
|
11332
|
-
};
|
|
11333
|
-
for (const t of todos) {
|
|
11334
|
-
if (!t || typeof t !== "object") continue;
|
|
11335
|
-
const status = t.status;
|
|
11336
|
-
if (typeof status === "string" && status in counts) counts[status] += 1;
|
|
11337
|
-
}
|
|
11338
|
-
const meta = [];
|
|
11339
|
-
if (counts.completed) meta.push(`${counts.completed} done`);
|
|
11340
|
-
if (counts.in_progress) meta.push(`${counts.in_progress} in progress`);
|
|
11341
|
-
if (counts.pending) meta.push(`${counts.pending} pending`);
|
|
11342
|
-
if (counts.cancelled) meta.push(`${counts.cancelled} cancelled`);
|
|
11343
|
-
return {
|
|
11344
|
-
target: `${todos.length} item${todos.length === 1 ? "" : "s"}`,
|
|
11345
|
-
meta
|
|
11346
|
-
};
|
|
11347
|
-
}
|
|
11348
|
-
},
|
|
11349
|
-
todoread: {
|
|
11350
|
-
displayName: "Todos",
|
|
11351
|
-
format: () => ({ target: "read" })
|
|
11352
|
-
},
|
|
11353
|
-
ask_user: {
|
|
11354
|
-
displayName: "Ask user",
|
|
11355
|
-
format: (input) => {
|
|
11356
|
-
const questions = Array.isArray(input.questions) ? input.questions.length : 0;
|
|
11357
|
-
if (questions === 0) return null;
|
|
11358
|
-
return { target: `${questions} question${questions === 1 ? "" : "s"}` };
|
|
11359
|
-
}
|
|
11360
|
-
},
|
|
11361
|
-
present_plan: {
|
|
11362
|
-
displayName: "Present plan",
|
|
11363
|
-
format: (input) => {
|
|
11364
|
-
const title = stringField(input, "title");
|
|
11365
|
-
if (!title) return null;
|
|
11366
|
-
return { target: title };
|
|
11367
|
-
}
|
|
11368
|
-
}
|
|
11369
|
-
};
|
|
11370
|
-
/**
|
|
11371
|
-
* Resolve the display verb for a tool. Native tools use their curated
|
|
11372
|
-
* entry from {@link TOOL_DISPLAY}; everything else gets a sentence-case
|
|
11373
|
-
* version of the raw name (`my_host_tool` → `My host tool`) so an MCP /
|
|
11374
|
-
* host tool still reads cleanly in the transcript without shouting
|
|
11375
|
-
* Title Case at every word.
|
|
11376
|
-
*
|
|
11377
|
-
* MCP convention: every tool surfaced by `mcp/connectMcpServers` is
|
|
11378
|
-
* namespaced as `mcp_<server>_<tool>` (see `src/mcp/index.ts`). The
|
|
11379
|
-
* `mcp_` prefix is plumbing — strip it before casing so the label
|
|
11380
|
-
* reads as `Github create issue` instead of `Mcp github create issue`.
|
|
11381
|
-
* The server name leads, which doubles as a free visual grouping
|
|
11382
|
-
* affordance ("everything starting with `Github` came from the github
|
|
11383
|
-
* MCP server").
|
|
11384
|
-
*/
|
|
11385
|
-
function displayNameFor(name, input) {
|
|
11386
|
-
const entry = TOOL_DISPLAY[name];
|
|
11387
|
-
if (entry) return typeof entry.displayName === "function" ? entry.displayName(input) : entry.displayName;
|
|
11388
|
-
return sentenceCase(name.startsWith("mcp_") ? name.slice(4) : name);
|
|
11389
|
-
}
|
|
11390
|
-
/**
|
|
11391
|
-
* Run a tool's curated formatter and return the result, or `null` when
|
|
11392
|
-
* no formatter is registered / the input shape doesn't match. Renderer
|
|
11393
|
-
* decides what to do with `null` — typically: show `↳ <displayName>`
|
|
11394
|
-
* with no target / meta tail.
|
|
11395
|
-
*/
|
|
11396
|
-
function formatToolCall(name, input) {
|
|
11397
|
-
const entry = TOOL_DISPLAY[name];
|
|
11398
|
-
if (!entry) return null;
|
|
11399
|
-
try {
|
|
11400
|
-
return entry.format(input);
|
|
11401
|
-
} catch {
|
|
11402
|
-
return null;
|
|
11403
|
-
}
|
|
11404
|
-
}
|
|
11405
|
-
function stringField(input, key) {
|
|
11406
|
-
const v = input[key];
|
|
11407
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
11408
|
-
}
|
|
11409
|
-
function numberField(input, key) {
|
|
11410
|
-
const v = input[key];
|
|
11411
|
-
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
11412
|
-
}
|
|
11413
|
-
/** `snake_case` / `kebab-case` / lowercase → `Sentence case`. */
|
|
11414
|
-
function sentenceCase(s) {
|
|
11415
|
-
const words = s.split(/[-_\s]+/).filter(Boolean).map((w) => w.toLowerCase());
|
|
11416
|
-
if (words.length === 0) return "";
|
|
11417
|
-
words[0] = (words[0][0]?.toUpperCase() ?? "") + words[0].slice(1);
|
|
11418
|
-
return words.join(" ");
|
|
11419
|
-
}
|
|
11420
|
-
/**
|
|
11421
|
-
* Collapse internal whitespace (including newlines) to single spaces
|
|
11422
|
-
* and clip to `max` columns with a trailing `…`. The whitespace
|
|
11423
|
-
* normalisation is the load-bearing bit — tool input strings like a
|
|
11424
|
-
* shell heredoc or a multi-line Python `-c` script otherwise render
|
|
11425
|
-
* across several rows in the transcript even though the `↳ Tool …`
|
|
11426
|
-
* line is meant to be a single-line scannable summary.
|
|
11427
|
-
*/
|
|
11428
|
-
function truncate(s, max) {
|
|
11429
|
-
const clean = s.replace(/\s+/g, " ").trim();
|
|
11430
|
-
return clean.length <= max ? clean : `${clean.slice(0, max - 1)}…`;
|
|
11431
|
-
}
|
|
11432
|
-
function byteLengthUtf8(s) {
|
|
11433
|
-
let bytes = 0;
|
|
11434
|
-
for (let i = 0; i < s.length; i++) {
|
|
11435
|
-
const code = s.charCodeAt(i);
|
|
11436
|
-
if (code < 128) bytes += 1;
|
|
11437
|
-
else if (code < 2048) bytes += 2;
|
|
11438
|
-
else if (code >= 55296 && code <= 56319) {
|
|
11439
|
-
bytes += 4;
|
|
11440
|
-
i++;
|
|
11441
|
-
} else bytes += 3;
|
|
11442
|
-
}
|
|
11443
|
-
return bytes;
|
|
11444
|
-
}
|
|
11445
|
-
function formatBytes(bytes) {
|
|
11446
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
11447
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
11448
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
11449
|
-
}
|
|
11450
|
-
//#endregion
|
|
11451
9000
|
//#region src/chat/transcript-anchors.ts
|
|
11452
9001
|
/**
|
|
11453
9002
|
* Per-item anchor ids for auto-scroll. Walks `items` in render order
|
|
@@ -11488,121 +9037,6 @@ function computeTurnAnchors(items) {
|
|
|
11488
9037
|
};
|
|
11489
9038
|
}
|
|
11490
9039
|
//#endregion
|
|
11491
|
-
|
|
11492
|
-
/**
|
|
11493
|
-
* Fork — keep every turn up to and including `turnId`, then strip any
|
|
11494
|
-
* `tool_call` blocks left without a matching `tool_result` in the slice.
|
|
11495
|
-
*
|
|
11496
|
-
* Semantics:
|
|
11497
|
-
* - Include the selected turn ("branch from HERE" mental model — the
|
|
11498
|
-
* user wants the selected message to be the latest in the fork).
|
|
11499
|
-
* - If the selected turn is an assistant turn with unresolved
|
|
11500
|
-
* `tool_call` blocks (their `tool_result`s live in turns AFTER the
|
|
11501
|
-
* slice), strip those calls. Otherwise the fork would post an
|
|
11502
|
-
* assistant turn with no matching tool results, breaking the next
|
|
11503
|
-
* provider call.
|
|
11504
|
-
* - Drop turns that become empty (all blocks stripped).
|
|
11505
|
-
*
|
|
11506
|
-
* Returns `null` when `turnId` doesn't exist in `turns` — caller should
|
|
11507
|
-
* surface a "turn not found" error rather than silently no-op.
|
|
11508
|
-
*/
|
|
11509
|
-
function truncateTurnsAt(turns, turnId) {
|
|
11510
|
-
const idx = turns.findIndex((t) => t.id === turnId);
|
|
11511
|
-
if (idx === -1) return null;
|
|
11512
|
-
return stripOrphanToolBlocks(turns.slice(0, idx + 1));
|
|
11513
|
-
}
|
|
11514
|
-
/**
|
|
11515
|
-
* Delete — remove the turn with `turnId` and any tool blocks left
|
|
11516
|
-
* orphaned by the removal. Returns `null` when `turnId` doesn't exist.
|
|
11517
|
-
*
|
|
11518
|
-
* Strategy:
|
|
11519
|
-
* 1. Drop the target turn.
|
|
11520
|
-
* 2. Scan the remaining turns for `tool_call`s without a matching
|
|
11521
|
-
* `tool_result` (orphaned by removing the user turn that carried
|
|
11522
|
-
* the result), and `tool_result`s without a matching `tool_call`
|
|
11523
|
-
* (orphaned by removing the assistant turn that issued the call).
|
|
11524
|
-
* Strip both sides.
|
|
11525
|
-
* 3. Drop turns whose content is now empty.
|
|
11526
|
-
*
|
|
11527
|
-
* This guarantees the resulting history is protocol-clean — a follow-up
|
|
11528
|
-
* `agent.run()` against the modified session can post turns without the
|
|
11529
|
-
* provider rejecting the history.
|
|
11530
|
-
*/
|
|
11531
|
-
function deleteTurnSafely(turns, turnId) {
|
|
11532
|
-
const idx = turns.findIndex((t) => t.id === turnId);
|
|
11533
|
-
if (idx === -1) return null;
|
|
11534
|
-
return stripOrphanToolBlocks([...turns.slice(0, idx), ...turns.slice(idx + 1)]);
|
|
11535
|
-
}
|
|
11536
|
-
/**
|
|
11537
|
-
* Walk a turn list and remove any tool blocks whose counterpart is
|
|
11538
|
-
* missing. Drops turns left empty. Used by `truncateTurnsAt` (which can
|
|
11539
|
-
* leave `tool_call`s orphaned when their results are past the cut) and
|
|
11540
|
-
* `deleteTurnSafely` (which can orphan either side of a pair).
|
|
11541
|
-
*
|
|
11542
|
-
* Pure / total: returns a new array; never throws.
|
|
11543
|
-
*/
|
|
11544
|
-
function stripOrphanToolBlocks(turns) {
|
|
11545
|
-
const callIds = /* @__PURE__ */ new Set();
|
|
11546
|
-
const resultIds = /* @__PURE__ */ new Set();
|
|
11547
|
-
for (const turn of turns) for (const block of turn.content) if (block.type === "tool_call") callIds.add(block.id);
|
|
11548
|
-
else if (block.type === "tool_result") resultIds.add(block.callId);
|
|
11549
|
-
const result = [];
|
|
11550
|
-
for (const turn of turns) {
|
|
11551
|
-
const filtered = [];
|
|
11552
|
-
for (const block of turn.content) {
|
|
11553
|
-
if (block.type === "tool_call") {
|
|
11554
|
-
if (!resultIds.has(block.id)) continue;
|
|
11555
|
-
} else if (block.type === "tool_result") {
|
|
11556
|
-
if (!callIds.has(block.callId)) continue;
|
|
11557
|
-
}
|
|
11558
|
-
filtered.push(block);
|
|
11559
|
-
}
|
|
11560
|
-
if (filtered.length === 0) continue;
|
|
11561
|
-
result.push(filtered.length === turn.content.length ? turn : {
|
|
11562
|
-
...turn,
|
|
11563
|
-
content: filtered
|
|
11564
|
-
});
|
|
11565
|
-
}
|
|
11566
|
-
return result;
|
|
11567
|
-
}
|
|
11568
|
-
/**
|
|
11569
|
-
* Serialize a turn's content to a clean text representation suited for
|
|
11570
|
-
* the clipboard. Joins text + thinking blocks verbatim; tool calls and
|
|
11571
|
-
* tool results get bracketed labels so the user can paste a readable
|
|
11572
|
-
* record of what happened without losing structure.
|
|
11573
|
-
*
|
|
11574
|
-
* Empty turns return `''`.
|
|
11575
|
-
*/
|
|
11576
|
-
function turnAsText(turn) {
|
|
11577
|
-
const parts = [];
|
|
11578
|
-
for (const block of turn.content) if (block.type === "text" && block.text.trim()) parts.push(block.text);
|
|
11579
|
-
else if (block.type === "thinking" && block.text.trim()) parts.push(`[thinking]\n${block.text}`);
|
|
11580
|
-
else if (block.type === "tool_call") parts.push(`[tool call · ${block.name}]\n${stringifyArgs(block.input)}`);
|
|
11581
|
-
else if (block.type === "tool_result") parts.push(`[tool result]\n${typeof block.output === "string" ? block.output : JSON.stringify(block.output, null, 2)}`);
|
|
11582
|
-
else if (block.type === "compact-summary") parts.push(`[compaction summary · ${block.replacesTurnIds.length} turn${block.replacesTurnIds.length === 1 ? "" : "s"}]\n${block.summary}`);
|
|
11583
|
-
return parts.join("\n\n");
|
|
11584
|
-
}
|
|
11585
|
-
function stringifyArgs(input) {
|
|
11586
|
-
try {
|
|
11587
|
-
return JSON.stringify(input, null, 2);
|
|
11588
|
-
} catch {
|
|
11589
|
-
return String(input);
|
|
11590
|
-
}
|
|
11591
|
-
}
|
|
11592
|
-
/**
|
|
11593
|
-
* Count turns before / after the one identified by `turnId` in the
|
|
11594
|
-
* given list. Returns `null` when the id is missing. Used to label the
|
|
11595
|
-
* turn-details modal with `N before · M after`.
|
|
11596
|
-
*/
|
|
11597
|
-
function countNeighbors(turnIds, turnId) {
|
|
11598
|
-
const idx = turnIds.indexOf(turnId);
|
|
11599
|
-
if (idx === -1) return null;
|
|
11600
|
-
return {
|
|
11601
|
-
before: idx,
|
|
11602
|
-
after: turnIds.length - 1 - idx
|
|
11603
|
-
};
|
|
11604
|
-
}
|
|
11605
|
-
//#endregion
|
|
11606
|
-
export { mcpToolsCachePath as $, KEYBINDING_DEF_BY_ACTION as $n, getModelInfo as $r, CATPPUCCIN_FRAPPE as $t, getSafelist as A, useActiveTodos as Ai, turnSelectionOwnership as An, detectLibc as Ar, clipHintsToWidth as At, oauthUsesManualCodePaste as B, TOKEN_DISCIPLINE_DOCTRINE as Bi, splitLines as Bn, credentialsPath as Br, SETTINGS_CATEGORIES as Bt, resolveSessionExportTarget as C, getArchivedTodosForRun as Ci, saveState as Cn, tryOpenBrowser as Cr, isInteractionTool as Ct, useSafeModeQueue as D, pruneTodosByRun as Di, titleFromTurns as Dn, useUpdateCheck as Dr, useInteractionsActions as Dt, useSafeModeActions as E, pickActiveRunId as Ei, sumRunCosts as En, buildUpdateHint as Er, serializeInteractionResponse as Et, suggestSafelistEntry as F, INTERACTION_GUIDANCE as Fi, computeInlineDiff as Fn, resolvePlatformPackage as Fr, buildHints as Ft, indexOfEntry as G, mergeApprovalAndBodyOutcomes as Gn, writeCredentials as Gr, useSettings as Gt, supportsOAuth as H, buildPlanSystem as Hi, tokenize as Hn, readProviderCredential as Hr, SETTINGS_TOGGLES as Ht, writeProjects as I, INTERACTION_GUIDANCE_NO_PROMPTS as Ii, computeLineDiff as In, AUTO_COMPACT_MIN_GROWTH_FRACTION as Ir, shortChord as It, discoverProjectMcps as J, rewriteMultiEditHeader as Jn, anthropicDescriptor as Jr, resolveChipColor as Jt, buildMcpServers as K, parseEditOutcomesFromResult as Kn, BUILTIN_PROVIDERS as Kr, BUILTIN_THEMES as Kt, splitPromptSegments as L, PLAN_MODE_DOCTRINE as Li, extractEditPayload as Ln, shouldAutoCompact as Lr, listProjectFiles as Lt, matchesSafelistEntry as M, COMMUNICATION_DOCTRINE as Mi, applyEditPayload as Mn, parseSemver as Mr, truncateTrailing as Mt, projectsFilePath as N, DOING_TASKS_DOCTRINE as Ni, buildContextualDiff as Nn, performInPlaceSelfUpdate as Nr, cleanTitle as Nt, IMPLICITLY_SAFE_TOOLS as O, selectActiveTodos as Oi, toolCallPreview as On, checkForUpdate as Or, useInteractionsQueue as Ot, readProjects as P, IDENTITY_PREFIX as Pi, buildUnifiedDiff as Pn, performSelfUpdate as Pr, generateSessionTitle as Pt, loadMcpToolsCache as Q, KEYBINDING_DEFS as Qn, getContextWindow as Qr, GRUVBOX_LIGHT as Qt, formatPathForCwd as R, PLAN_MODE_DOCTRINE_NO_PROMPTS as Ri, filetypeFromPath as Rn, detectAuth as Rr, useEnabledToggleSet as Rt, renderSession as S, createTodoTools as Si, marginTopFor as Sn, buildLinearRamp as Sr, createInteractionTools as St, SafeModeProvider as T, isTodoTool as Ti, stripSpawnTokensLine as Tn, bootTick as Tr, pendingInteractionsFromTurns as Tt, buildModelCatalog as U, envSection as Ui, buildEditOutcomesAnnotation as Un, removeProviderCredential as Ur, SettingsProvider as Ut, runOAuthLogin as V, buildBuildSystem as Vi, summarizeEditPayload as Vn, readCredentials as Vr, SETTINGS_CHOICES as Vt, filterModelCatalog as W, maskToOutcomeKinds as Wn, setProviderCredential as Wr, clampFps as Wt, projectUserPaths as X, summarizeOutcomes as Xn, credKeyOf as Xr, VAPORWAVE_THEME as Xt, parseMcpsFile as Y, stripEditOutcomesAnnotation as Yn, cerebrasDescriptor as Yr, resolveTheme as Yt, clearMcpToolsCache as Z, DEFAULT_KEYBINDINGS as Zn, effectiveContextWindow as Zr, GRUVBOX_DARK as Zt, turnContextSize as _, TODOREAD_TOOL as _i, isTurnHighlighted as _n, collectReferences as _r, splitMarkdownCodeBlocks as _t, computeTurnAnchors as a, discoverAgentsMd as ai, useDiscovery as an, matchesBinding as ar, useMcpToolToggleSet as at, defaultSkillScanPaths as b, TODO_STATUS_GLYPHS as bi, listSessionMeta as bn, useCompletion as br, PRESENT_PLAN_TOOL as bt, formatToolCall as c, BUILD_AGENT as ci, useConfig as cn, readKeybindings as cr, parentServerName as ct, useSelectStyle as d, DEFAULT_BUDGET_EXCLUDE_TOOLS as di, resolveStorageDirs as dn, createSkillsCompletionProvider as dr, patchMcpCredential as dt, modelSupportsReasoning as ei, CATPPUCCIN_LATTE as en, KEYBINDING_KEY_COL_WIDTH as er, refreshMcpToolsCatalog as et, useSurfaces as f, DEFAULT_PERSIST_EXCLUDE_TOOLS as fi, EDIT_TOOL_NAMES as fn, uniqueSkillNamesFromReferences as fr, McpAuthProvider as ft, finalizeStreamingMarkdownForOwner as g, singleAgentRegistry as gi, isEditErrorResult as gn, applyInsert as gr, reduceMcpAuth as gt, finalizeStreamingMarkdown as h, resolveAgentId as hi, eventsFromTurns as hn, uniqueFilesFromReferences as hr, getMcpAuthStatus as ht, turnAsText as i, piIdOf as ii, DiscoveryProvider as in, keybindingsPath as ir, useMcpToolToggleMap as it, isOnSafelist as j, ACTIONS_WITH_CARE_DOCTRINE as ji, updateToolEventOutcomes as jn, detectPackageManager as jr, hintsLength as jt, addToSafelist as k, setTodosForRun as ki, toolResultText as kn, compareSemver as kr, EMPTY_HINTS as kt, ThemeProvider as l, BUILTIN_AGENTS as li, resolveConfig as ln, stripJsonComments as lr, createFileMcpCredentialStore as lt, useTheme as m, accentColor as mi, deriveSessionTitle as mn, createFilesCompletionProvider as mr, useMcpAuthState as mt, deleteTurnSafely as n, openaiDescriptor as ni, CATPPUCCIN_MOCHA as nn, formatBindingForDisplay as nr, subscribeMcpToolsCache as nt, TOOL_DISPLAY as o, renderAgentsMdBlock as oi, useDiscoveryOptional as on, mergeKeybindings as or, buildVisibleMcpRows as ot, useSyntaxStyles as p, PLAN_AGENT as pi, createStateStore as pn, FILES_TRIGGER as pr, useMcpAuthDispatch as pt, defaultMcpsConfigPaths as q, resolveApprovalForPayload as qn, OUTPUT_RESERVE_TOKENS as qr, DEFAULT_THEME as qt, truncateTurnsAt as r, openrouterDescriptor as ri, createDiscoverySlot as rn, groupBindings as rr, buildToolToggle as rt, displayNameFor as s, findGitRoot$1 as si, ConfigProvider as sn, parseBindingSpec as sr, indexOfServerRow as st, countNeighbors as t, modelsForDescriptor as ti, CATPPUCCIN_MACCHIATO as tn, ensureKeybindingsFile as tr, saveMcpToolsCache as tt, useColors as u, DEFAULT_AGENT_ID as ui, resolveStoragePaths as un, SKILLS_TRIGGER as ur, mcpCredentialsPath as ut, useStreamBuffer as v, TODOS_METADATA_KEY as vi, isVisible as vn, findActiveTrigger as vr, ASK_USER_TOOL as vt, writeSessionExport as w, getTodosForRun as wi, selectableTurnIds as wn, bootProfileEnabled as wr, makeRequestInteraction as wt, discoverProjectSkills as x, TODO_WRITE_COUNTS_METADATA_KEY as xi, loadState as xn, blendHsl as xr, buildResumedToolResultsTurn as xt, buildSkillsConfig as y, TODOWRITE_TOOL as yi, lastContextSizeFromTurns as yn, mergeReferences as yr, InteractionsProvider as yt, fetchOAuthRedirect as z, SUBAGENT_GUIDANCE as zi, previewEditPayload as zn, applyApiKeyEnv as zr, DEFAULT_SETTINGS as zt };
|
|
9040
|
+
export { useMcpAuthDispatch as $, BUILD_AGENT as $n, deriveSessionTitle as $t, runOAuthLogin as A, buildUpdateHint as An, SUBAGENT_GUIDANCE as Ar, clampFps as At, refreshMcpToolsCatalog as B, AUTO_COMPACT_MIN_GROWTH_FRACTION as Bn, CATPPUCCIN_LATTE as Bt, projectsFilePath as C, parseBindingSpec as Cn, COMMUNICATION_DOCTRINE as Cr, listProjectFiles as Ct, formatPathForCwd as D, tryOpenBrowser as Dn, INTERACTION_GUIDANCE_NO_PROMPTS as Dr, SETTINGS_CHOICES as Dt, writeProjects as E, useCompletion as En, INTERACTION_GUIDANCE as Er, SETTINGS_CATEGORIES as Et, parseMcpsFile as F, detectPackageManager as Fn, resolveTheme as Ft, useMcpToolToggleSet as G, readCredentials as Gn, useDiscovery as Gt, subscribeMcpToolsCache as H, detectAuth as Hn, CATPPUCCIN_MOCHA as Ht, projectUserPaths as I, parseSemver as In, VAPORWAVE_THEME as It, parentServerName as J, setProviderCredential as Jn, useConfig as Jt, buildVisibleMcpRows as K, readProviderCredential as Kn, useDiscoveryOptional as Kt, clearMcpToolsCache as L, performInPlaceSelfUpdate as Ln, GRUVBOX_DARK as Lt, buildMcpServers as M, checkForUpdate as Mn, buildBuildSystem as Mr, BUILTIN_THEMES as Mt, defaultMcpsConfigPaths as N, compareSemver as Nn, buildPlanSystem as Nr, DEFAULT_THEME as Nt, fetchOAuthRedirect as O, bootProfileEnabled as On, PLAN_MODE_DOCTRINE as Or, SETTINGS_TOGGLES as Ot, discoverProjectMcps as P, detectLibc as Pn, envSection as Pr, resolveChipColor as Pt, McpAuthProvider as Q, findGitRoot$1 as Qn, createStateStore as Qt, loadMcpToolsCache as R, performSelfUpdate as Rn, GRUVBOX_LIGHT as Rt, matchesSafelistEntry as S, mergeKeybindings as Sn, ACTIONS_WITH_CARE_DOCTRINE as Sr, shortChord as St, suggestSafelistEntry as T, stripJsonComments as Tn, IDENTITY_PREFIX as Tr, DEFAULT_SETTINGS as Tt, buildToolToggle as U, applyApiKeyEnv as Un, createDiscoverySlot as Ut, saveMcpToolsCache as V, shouldAutoCompact as Vn, CATPPUCCIN_MACCHIATO as Vt, useMcpToolToggleMap as W, credentialsPath as Wn, DiscoveryProvider as Wt, mcpCredentialsPath as X, discoverAgentsMd as Xn, resolveStoragePaths as Xt, createFileMcpCredentialStore as Y, writeCredentials as Yn, resolveConfig as Yt, patchMcpCredential as Z, renderAgentsMdBlock as Zn, resolveStorageDirs as Zt, useSafeModeQueue as _, ensureKeybindingsFile as _n, pickActiveRunId as _r, hintsLength as _t, useSurfaces as a, saveState as an, accentColor as ar, InteractionsProvider as at, getSafelist as b, keybindingsPath as bn, setTodosForRun as br, generateSessionTitle as bt, useStreamBuffer as c, sumRunCosts as cn, TODOREAD_TOOL as cr, createInteractionTools as ct, discoverProjectSkills as d, toolResultText as dn, TODO_STATUS_GLYPHS as dr, pendingInteractionsFromTurns as dt, eventsFromTurns as en, BUILTIN_AGENTS as er, useMcpAuthState as et, renderSession as f, updateToolEventOutcomes as fn, TODO_WRITE_COUNTS_METADATA_KEY as fr, serializeInteractionResponse as ft, useSafeModeActions as g, KEYBINDING_KEY_COL_WIDTH as gn, isTodoTool as gr, clipHintsToWidth as gt, SafeModeProvider as h, KEYBINDING_DEF_BY_ACTION as hn, getTodosForRun as hr, EMPTY_HINTS as ht, useSelectStyle as i, marginTopFor as in, PLAN_AGENT as ir, ASK_USER_TOOL as it, supportsOAuth as j, useUpdateCheck as jn, TOKEN_DISCIPLINE_DOCTRINE as jr, useSettings as jt, oauthUsesManualCodePaste as k, bootTick as kn, PLAN_MODE_DOCTRINE_NO_PROMPTS as kr, SettingsProvider as kt, buildSkillsConfig as l, titleFromTurns as ln, TODOS_METADATA_KEY as lr, isInteractionTool as lt, writeSessionExport as m, KEYBINDING_DEFS as mn, getArchivedTodosForRun as mr, useInteractionsQueue as mt, ThemeProvider as n, listSessionMeta as nn, DEFAULT_BUDGET_EXCLUDE_TOOLS as nr, reduceMcpAuth as nt, useSyntaxStyles as o, serverToolResultSummary as on, resolveAgentId as or, PRESENT_PLAN_TOOL as ot, resolveSessionExportTarget as p, DEFAULT_KEYBINDINGS as pn, createTodoTools as pr, useInteractionsActions as pt, indexOfServerRow as q, removeProviderCredential as qn, ConfigProvider as qt, useColors as r, loadState as rn, DEFAULT_PERSIST_EXCLUDE_TOOLS as rr, splitMarkdownCodeBlocks as rt, useTheme as s, stripSpawnTokensLine as sn, singleAgentRegistry as sr, buildResumedToolResultsTurn as st, computeTurnAnchors as t, lastContextSizeFromTurns as tn, DEFAULT_AGENT_ID as tr, getMcpAuthStatus as tt, defaultSkillScanPaths as u, toolCallPreview as un, TODOWRITE_TOOL as ur, makeRequestInteraction as ut, IMPLICITLY_SAFE_TOOLS as v, formatBindingForDisplay as vn, pruneTodosByRun as vr, truncateTrailing as vt, readProjects as w, readKeybindings as wn, DOING_TASKS_DOCTRINE as wr, useEnabledToggleSet as wt, isOnSafelist as x, matchesBinding as xn, useActiveTodos as xr, buildHints as xt, addToSafelist as y, groupBindings as yn, selectActiveTodos as yr, cleanTitle as yt, mcpToolsCachePath as z, resolvePlatformPackage as zn, CATPPUCCIN_FRAPPE as zt };
|
|
11607
9041
|
|
|
11608
|
-
//# sourceMappingURL=
|
|
9042
|
+
//# sourceMappingURL=transcript-anchors-BTSZAPVc.js.map
|