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.
Files changed (119) hide show
  1. package/README.md +3 -1
  2. package/dist/{agent-ClkpElCZ.d.ts → agent-BNS2nx_T.d.ts} +535 -15
  3. package/dist/agent-BNS2nx_T.d.ts.map +1 -0
  4. package/dist/chat/pure.d.ts +4 -0
  5. package/dist/chat/pure.js +3 -0
  6. package/dist/chat.d.ts +31 -661
  7. package/dist/chat.d.ts.map +1 -1
  8. package/dist/chat.js +5 -3
  9. package/dist/chat.js.map +1 -1
  10. package/dist/contexts/docker.d.ts +1 -1
  11. package/dist/contexts/docker.d.ts.map +1 -1
  12. package/dist/contexts/docker.js.map +1 -1
  13. package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
  14. package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
  15. package/dist/contexts.d.ts +3 -3
  16. package/dist/contexts.js +1 -1
  17. package/dist/edit-utils-DnfNoj16.js +574 -0
  18. package/dist/edit-utils-DnfNoj16.js.map +1 -0
  19. package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
  20. package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
  21. package/dist/fetch-url-CPxfiXDa.js +518 -0
  22. package/dist/fetch-url-CPxfiXDa.js.map +1 -0
  23. package/dist/image-sniff-B7uFSNO1.js +90 -0
  24. package/dist/image-sniff-B7uFSNO1.js.map +1 -0
  25. package/dist/{index-CbS75MD3.d.ts → index-CZOwAJIX.d.ts} +2 -2
  26. package/dist/index-CZOwAJIX.d.ts.map +1 -0
  27. package/dist/{index-CTDMMdIy.d.ts → index-Ck_AWt8P.d.ts} +3 -4
  28. package/dist/index-Ck_AWt8P.d.ts.map +1 -0
  29. package/dist/{index-v3Tzobqr.d.ts → index-KiS7w0dC.d.ts} +3 -3
  30. package/dist/index-KiS7w0dC.d.ts.map +1 -0
  31. package/dist/index.d.ts +6 -6
  32. package/dist/index.js +13 -12
  33. package/dist/index.js.map +1 -1
  34. package/dist/{interpolate-DM1UcKeQ.js → interpolate-TySiqKzc.js} +23 -23
  35. package/dist/{interpolate-DM1UcKeQ.js.map → interpolate-TySiqKzc.js.map} +1 -1
  36. package/dist/{login-7tHcckmX.js → login-BDeqENSe.js} +7 -58
  37. package/dist/login-BDeqENSe.js.map +1 -0
  38. package/dist/{mcp-DGeB7-3D.js → mcp-Kqzz-Rs_.js} +8 -6
  39. package/dist/mcp-Kqzz-Rs_.js.map +1 -0
  40. package/dist/mcp.d.ts +2 -2
  41. package/dist/mcp.js +1 -1
  42. package/dist/{messages-Dym8S_YH.js → messages-CvRQTdbR.js} +118 -39
  43. package/dist/messages-CvRQTdbR.js.map +1 -0
  44. package/dist/{presets-w9Px_aAm.js → presets-JuOnSI-i.js} +2 -2
  45. package/dist/{presets-w9Px_aAm.js.map → presets-JuOnSI-i.js.map} +1 -1
  46. package/dist/presets.d.ts +3 -3
  47. package/dist/presets.js +1 -1
  48. package/dist/{providers-beXyD9W9.js → providers-h4HJPbbv.js} +485 -31
  49. package/dist/providers-h4HJPbbv.js.map +1 -0
  50. package/dist/providers.d.ts +2 -2
  51. package/dist/providers.js +3 -3
  52. package/dist/restate.d.ts +1 -1
  53. package/dist/restate.d.ts.map +1 -1
  54. package/dist/restate.js.map +1 -1
  55. package/dist/session/sqlite.d.ts +1 -1
  56. package/dist/session/sqlite.d.ts.map +1 -1
  57. package/dist/session/sqlite.js +1 -1
  58. package/dist/session/sqlite.js.map +1 -1
  59. package/dist/{session-BRIsmBSY.js → session-BzLou2_-.js} +2 -2
  60. package/dist/{session-BRIsmBSY.js.map → session-BzLou2_-.js.map} +1 -1
  61. package/dist/session.d.ts +2 -2
  62. package/dist/session.js +2 -2
  63. package/dist/skills.d.ts +3 -3
  64. package/dist/skills.js +1 -1
  65. package/dist/skills.js.map +1 -1
  66. package/dist/{stats-Lc3zL3RM.js → stats-DAKBEKjc.js} +12 -2
  67. package/dist/stats-DAKBEKjc.js.map +1 -0
  68. package/dist/{stdio-loader-EVAF5KlU.js → stdio-loader-Ce68wUmM.js} +4 -4
  69. package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
  70. package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
  71. package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
  72. package/dist/tools/fetch-url.d.ts +70 -0
  73. package/dist/tools/fetch-url.d.ts.map +1 -0
  74. package/dist/tools/fetch-url.js +2 -0
  75. package/dist/tools/web-search.d.ts +7 -0
  76. package/dist/tools/web-search.d.ts.map +1 -0
  77. package/dist/tools/web-search.js +190 -0
  78. package/dist/tools/web-search.js.map +1 -0
  79. package/dist/{tools-DhrLrOEr.js → tools-BGtJK0vo.js} +1368 -421
  80. package/dist/tools-BGtJK0vo.js.map +1 -0
  81. package/dist/tools.d.ts +3 -3
  82. package/dist/tools.js +1 -1
  83. package/dist/{turn-operations-UAkOjO-u.js → transcript-anchors-BTSZAPVc.js} +147 -2713
  84. package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
  85. package/dist/{transcript-anchors-D0TR6djV.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
  86. package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
  87. package/dist/tui.d.ts +58 -28
  88. package/dist/tui.d.ts.map +1 -1
  89. package/dist/tui.js +1349 -422
  90. package/dist/tui.js.map +1 -1
  91. package/dist/turn-operations-CCHfR9eC.js +1938 -0
  92. package/dist/turn-operations-CCHfR9eC.js.map +1 -0
  93. package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
  94. package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
  95. package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
  96. package/dist/types-BPw_i5vb.js.map +1 -0
  97. package/dist/{types-KukEp-mi.d.ts → types-CEAMIUXw.d.ts} +1 -1
  98. package/dist/types-CEAMIUXw.d.ts.map +1 -0
  99. package/dist/types.d.ts +4 -4
  100. package/dist/types.js +3 -3
  101. package/docs/CHAT.md +53 -6
  102. package/docs/SKILL.md +3 -0
  103. package/docs/TUI.md +7 -0
  104. package/package.json +18 -2
  105. package/dist/agent-ClkpElCZ.d.ts.map +0 -1
  106. package/dist/index-CTDMMdIy.d.ts.map +0 -1
  107. package/dist/index-CbS75MD3.d.ts.map +0 -1
  108. package/dist/index-v3Tzobqr.d.ts.map +0 -1
  109. package/dist/login-7tHcckmX.js.map +0 -1
  110. package/dist/mcp-DGeB7-3D.js.map +0 -1
  111. package/dist/messages-Dym8S_YH.js.map +0 -1
  112. package/dist/providers-beXyD9W9.js.map +0 -1
  113. package/dist/stats-Lc3zL3RM.js.map +0 -1
  114. package/dist/stdio-loader-EVAF5KlU.js.map +0 -1
  115. package/dist/tools-DhrLrOEr.js.map +0 -1
  116. package/dist/transcript-anchors-D0TR6djV.d.ts.map +0 -1
  117. package/dist/turn-operations-UAkOjO-u.js.map +0 -1
  118. package/dist/types-KukEp-mi.d.ts.map +0 -1
  119. package/dist/types-oKPBdCmL.js.map +0 -1
@@ -1,23 +1,24 @@
1
- import { H as previewLine, R as fmtTokens, U as shortId, a as multiEdit, c as grep, d as resolveOldString, f as styleReplacementForVia, i as readFile$1, l as glob$1, n as createSpawnTool, o as listFiles, t as writeFile$1, u as edit, y as shell } from "./tools-DhrLrOEr.js";
2
- import { c as errorMessage } from "./errors-DdZXnyXE.js";
3
- import { r as toolResultToText } from "./types-oKPBdCmL.js";
4
- import { O as joinSystemPrompt } from "./messages-Dym8S_YH.js";
5
- import { r as normalizeMcpServers, t as connectMcpServers } from "./mcp-DGeB7-3D.js";
6
- import { a as discoverSkills } from "./interpolate-DM1UcKeQ.js";
7
- import { n as formatTokenUsage } from "./stats-Lc3zL3RM.js";
8
- import { n as definePreset, t as composePresets } from "./presets-w9Px_aAm.js";
9
- import { i as anthropic, n as openai, o as writeFileAtomic, r as cerebras, t as openrouter } from "./providers-beXyD9W9.js";
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 { homedir, tmpdir } from "node:os";
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 { Buffer as Buffer$1 } from "node:buffer";
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
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: "effort",
3863
- description: "open the reasoning-effort picker (when the active model supports it)",
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
- * Walk an LCS op stream and emit one summary entry per *run* of
4839
- * non-context ops, with the new-file line number where each run
4840
- * starts. Adjacent add/remove ops collapse into the same entry
4841
- * matches git's hunk grouping at zero context.
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 summarizeOpsByHunk(ops) {
4844
- const hunks = [];
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
- while (i < ops.length) {
4850
- if (ops[i].op === "context") {
4851
- nl++;
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
- const runStartLine = nl;
4856
- let added = 0;
4857
- let removed = 0;
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
- const match = resolveOldString(running, hunk.oldString);
4909
- if (!match) {
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
- const ambiguous = match.occurrences > 1 && !hunk.replaceAll;
4916
- const styledNew = styleReplacementForVia(hunk.newString, match.via, match.actual);
4917
- const resolvedHunk = {
4918
- oldString: match.actual,
4919
- newString: styledNew,
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 `${parts.join("\n")}\n`;
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
- return model ? {
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
- } : null;
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
- ...effortLabel ? { extra: {
6046
+ ...chordLabel ? { extra: {
8029
6047
  key: shortChord(keybindings.openEffortPicker),
8030
6048
  keyColor: effortKeyColor,
8031
- label: effortLabel,
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
- //#region src/chat/turn-operations.ts
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=turn-operations-UAkOjO-u.js.map
9042
+ //# sourceMappingURL=transcript-anchors-BTSZAPVc.js.map