@kernelonpanic/kitcode 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,8 +62,7 @@ var themeSchema = z.object({
62
62
  var langSchema = z.enum(["en", "ru"]);
63
63
  var budgetSchema = z.object({
64
64
  maxTokensPerTurn: z.number().int().min(0).max(1e7).default(1e7),
65
- maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0),
66
- maxSubagentsPerTurn: z.number().int().min(0).max(16).default(3)
65
+ maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0)
67
66
  });
68
67
  var diagnosticsSchema = z.object({
69
68
  autoRun: z.boolean().default(true),
@@ -79,8 +78,7 @@ var configSchema = z.object({
79
78
  maxTokens: z.number().int().positive().max(2e5).default(64e3),
80
79
  budget: budgetSchema.default({
81
80
  maxTokensPerTurn: 1e7,
82
- maxCostUsdPerTurn: 0,
83
- maxSubagentsPerTurn: 3
81
+ maxCostUsdPerTurn: 0
84
82
  }),
85
83
  diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
86
84
  providers: z.record(providerIdSchema, providerConfigSchema).default({}),
@@ -210,8 +208,8 @@ async function readHead(stream) {
210
208
  let text = "";
211
209
  try {
212
210
  while (text.length < CAPTURE_CHARS) {
213
- const { done, value } = await reader.read();
214
- if (done) break;
211
+ const { done: done2, value } = await reader.read();
212
+ if (done2) break;
215
213
  text += decoder.decode(value, { stream: true });
216
214
  }
217
215
  } catch {
@@ -420,39 +418,52 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
420
418
  `${anthropicBaseUrl}/v1/models?limit=1000`,
421
419
  anthropicHeaders(apiKey2)
422
420
  );
423
- return {
424
- id,
425
- config: { type: "anthropic", baseUrl: anthropicBaseUrl },
426
- models: probed.ok ? parseModelList(probed.body) : []
427
- };
421
+ return [
422
+ {
423
+ id,
424
+ config: { type: "anthropic", baseUrl: anthropicBaseUrl },
425
+ models: probed.ok ? parseModelList(probed.body) : []
426
+ }
427
+ ];
428
428
  }
429
+ const found = [];
429
430
  let last;
430
431
  for (const candidate of candidateBaseUrls(baseUrl)) {
432
+ if (found.length === schemes.length) break;
431
433
  const url = `${candidate}/models`;
432
- let attempt = await probe(fetchImpl, url, { Authorization: `Bearer ${apiKey2}` });
433
- last = attempt;
434
- if (attempt.ok && isModelListBody(attempt.body)) {
435
- const anthropic = advertisesAnthropicProtocol(attempt.body);
436
- return {
437
- id,
438
- config: anthropic ? { type: "anthropic", baseUrl: anthropicRoot(candidate) } : { type: "openai", baseUrl: candidate },
439
- models: parseModelList(attempt.body)
440
- };
441
- }
442
- if (attempt.status === 401 || attempt.status === 403) {
443
- attempt = await probe(fetchImpl, url, anthropicHeaders(apiKey2));
434
+ for (const scheme of schemes) {
435
+ const attempt = await probe(fetchImpl, url, schemeHeaders(scheme, apiKey2));
444
436
  last = attempt;
445
- if (attempt.ok) {
446
- return {
447
- id,
448
- config: { type: "anthropic", baseUrl: anthropicRoot(candidate) },
449
- models: parseModelList(attempt.body)
450
- };
451
- }
437
+ if (attempt.status === 404 || attempt.status === 0) break;
438
+ if (attempt.ok && isModelListBody(attempt.body)) record(found, candidate, scheme, attempt.body);
439
+ if (found.length === schemes.length) break;
452
440
  }
453
- if (!attempt.ok && attempt.status !== 404 && attempt.status !== 0) break;
441
+ if (found.length === 0 && !last.ok && last.status !== 404 && last.status !== 0) break;
454
442
  }
455
- throw new Error(describeFailure(last, apiKey2));
443
+ if (found.length === 0) throw new Error(describeFailure(last, apiKey2));
444
+ return found.map((entry, index) => ({
445
+ id: index === 0 ? id : `${id.slice(0, MAX_ID_STEM)}-${entry.type}`,
446
+ config: { type: entry.type, baseUrl: entry.baseUrl },
447
+ models: entry.models
448
+ }));
449
+ }
450
+ var schemes = ["bearer", "anthropic"];
451
+ var MAX_ID_STEM = 120;
452
+ function schemeHeaders(scheme, apiKey2) {
453
+ return scheme === "bearer" ? { Authorization: `Bearer ${apiKey2}` } : anthropicHeaders(apiKey2);
454
+ }
455
+ function record(found, candidate, scheme, body) {
456
+ const type = scheme === "anthropic" || advertisesAnthropicProtocol(body) ? "anthropic" : "openai";
457
+ if (found.some((entry) => entry.type === type)) return;
458
+ const models = parseModelList(body);
459
+ const fingerprint2 = models.map((model) => model.id).sort().join("\n");
460
+ if (found.some((entry) => entry.fingerprint === fingerprint2)) return;
461
+ found.push({
462
+ type,
463
+ baseUrl: type === "anthropic" ? anthropicRoot(candidate) : candidate,
464
+ models,
465
+ fingerprint: fingerprint2
466
+ });
456
467
  }
457
468
  function normaliseBaseUrl(rawUrl) {
458
469
  return rawUrl.trim().replace(/\/+$/, "");
@@ -519,8 +530,8 @@ async function boundedResponseText(response) {
519
530
  let text = "";
520
531
  try {
521
532
  for (; ; ) {
522
- const { done, value } = await reader.read();
523
- if (done) break;
533
+ const { done: done2, value } = await reader.read();
534
+ if (done2) break;
524
535
  bytes += value.byteLength;
525
536
  if (bytes > MAX_DETECT_BODY_BYTES) {
526
537
  throw new Error(`response exceeded ${MAX_DETECT_BODY_BYTES / 1e6} MB`);
@@ -566,11 +577,9 @@ function anthropicRoot(baseUrl) {
566
577
  function advertisesAnthropicProtocol(body) {
567
578
  const entries = modelEntries(body);
568
579
  if (entries.length === 0) return false;
569
- const endpointTypes = new Set(
570
- entries.flatMap((entry) => stringList(entry["supported_endpoint_types"]))
571
- );
572
- if (endpointTypes.size > 0) {
573
- return endpointTypes.has("anthropic") && !endpointTypes.has("openai");
580
+ const advertised = entries.map((entry) => stringList(entry["supported_endpoint_types"]));
581
+ if (advertised.some((types) => types.length > 0)) {
582
+ return advertised.every((types) => types.includes("anthropic"));
574
583
  }
575
584
  if (entries.some(isVendorNamespaced)) return false;
576
585
  return entries.every((entry) => entry["owned_by"] === "anthropic");
@@ -827,9 +836,9 @@ function conventionalKeyEnv(providerId) {
827
836
  function expandEnvRefs(value) {
828
837
  return value.replace(/\$\{env:([^}]+)\}/g, (_match, name) => process.env[name] ?? "");
829
838
  }
830
- function expandEnvRecord(record2) {
839
+ function expandEnvRecord(record3) {
831
840
  return Object.fromEntries(
832
- Object.entries(record2).map(([key, value]) => [key, expandEnvRefs(value)])
841
+ Object.entries(record3).map(([key, value]) => [key, expandEnvRefs(value)])
833
842
  );
834
843
  }
835
844
  async function readJson(file) {
@@ -895,12 +904,15 @@ async function addProvider(url, key, options = {}) {
895
904
  const auth = await loadAuth();
896
905
  const configBefore = structuredClone(config);
897
906
  const authBefore = { ...auth };
898
- config.providers[detected.id] = detected.config;
899
- auth[detected.id] = key;
900
- const models = detected.models;
907
+ for (const provider of detected) {
908
+ config.providers[provider.id] = provider.config;
909
+ auth[provider.id] = key;
910
+ }
911
+ const primary = detected[0];
912
+ const models = detected.flatMap((provider) => provider.models);
901
913
  if (!config.model) {
902
- const chosen = pickDefault(models);
903
- if (chosen) config.model = formatModelRef(detected.id, chosen);
914
+ const chosen = pickDefaultRef(detected);
915
+ if (chosen) config.model = chosen;
904
916
  }
905
917
  try {
906
918
  await saveConfig(config);
@@ -913,14 +925,15 @@ async function addProvider(url, key, options = {}) {
913
925
  throw error;
914
926
  }
915
927
  const location = await configLocation();
916
- const protocol = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
917
- const rows = [
918
- ["provider", `${detected.id} \u2014 this endpoint speaks the ${protocol} protocol`],
928
+ const rows = detected.map((provider) => [
929
+ "provider",
930
+ `${provider.id} \u2014 this endpoint speaks the ${protocolLabel(provider)} protocol`
931
+ ]);
932
+ rows.push(
919
933
  ["models", models.length === 0 ? "none listed by this endpoint" : `${models.length} available`],
920
- ["default", describeDefault(config.model, detected.id, models)]
921
- ];
922
- alternatives(models, config.model, detected.id).forEach((model, index) => {
923
- const ref = formatModelRef(detected.id, model.id);
934
+ ["default", describeDefault(config.model, detected)]
935
+ );
936
+ alternatives(detected, config.model).forEach(([ref, model], index) => {
924
937
  rows.push([index === 0 ? "also try" : "", `${ref} ${price(model)}`]);
925
938
  });
926
939
  rows.push(["config", `${location.path} (${location.scope})`]);
@@ -928,11 +941,15 @@ async function addProvider(url, key, options = {}) {
928
941
  rows.push(["next", "run kitcode to start a session; /model switches models"]);
929
942
  for (const [label, value] of rows) console.log(label.padEnd(LABEL_WIDTH) + value);
930
943
  }
931
- function describeDefault(ref, providerId, models) {
944
+ function protocolLabel(provider) {
945
+ return provider.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
946
+ }
947
+ function describeDefault(ref, detected) {
932
948
  if (!ref) return "not set \u2014 start kitcode and pick one with /model";
933
949
  const parsed = parseModelRef(ref);
934
- if (parsed?.provider !== providerId) return `${ref} \u2014 already in your config, left alone`;
935
- return `${ref} ${price(models.find((model) => model.id === parsed.model))}`;
950
+ const owner = detected.find((provider) => provider.id === parsed?.provider);
951
+ if (!owner) return `${ref} \u2014 already in your config, left alone`;
952
+ return `${ref} ${price(owner.models.find((model) => model.id === parsed.model))}`;
936
953
  }
937
954
  function price(model) {
938
955
  const pricing = model?.pricing;
@@ -942,25 +959,35 @@ function price(model) {
942
959
  function money(value) {
943
960
  return `$${value >= 1 ? value.toFixed(2) : Number(value.toFixed(4))}`;
944
961
  }
945
- function pickDefault(models) {
962
+ function pickDefaultRef(detected) {
963
+ const refs = modelRefs(detected);
946
964
  for (const preferred of PREFERRED) {
947
- const match = models.find((model) => model.id.endsWith(preferred));
948
- if (match) return match.id;
965
+ const match = refs.find(([, model]) => model.id.endsWith(preferred));
966
+ if (match) return match[0];
949
967
  }
950
- return models[0]?.id;
968
+ return refs[0]?.[0];
951
969
  }
952
- function alternatives(models, defaultRef, providerId) {
970
+ function alternatives(detected, defaultRef) {
971
+ const refs = modelRefs(detected);
953
972
  const ranked = [
954
- ...PREFERRED.flatMap((preferred) => models.filter((model) => model.id.endsWith(preferred))),
955
- ...models.filter((model) => model.pricing),
956
- ...models
973
+ ...PREFERRED.flatMap((preferred) => refs.filter(([, model]) => model.id.endsWith(preferred))),
974
+ ...refs.filter(([, model]) => model.pricing),
975
+ ...refs
957
976
  ];
958
977
  const picked = /* @__PURE__ */ new Map();
959
- for (const model of ranked) {
978
+ for (const [ref, model] of ranked) {
960
979
  if (picked.size === ALTERNATIVES) break;
961
- if (formatModelRef(providerId, model.id) !== defaultRef) picked.set(model.id, model);
980
+ if (ref !== defaultRef) picked.set(ref, model);
962
981
  }
963
- return [...picked.values()];
982
+ return [...picked.entries()];
983
+ }
984
+ function modelRefs(detected) {
985
+ return detected.flatMap(
986
+ (provider) => provider.models.map((model) => [
987
+ formatModelRef(provider.id, model.id),
988
+ model
989
+ ])
990
+ );
964
991
  }
965
992
 
966
993
  // src/app/ask.ts
@@ -1087,6 +1114,261 @@ ${store.read()}`, display: { kind: "diff", path: "project memory", before, after
1087
1114
  };
1088
1115
  }
1089
1116
 
1117
+ // src/ui/workspace-files.ts
1118
+ import fg2 from "fast-glob";
1119
+
1120
+ // src/tools/glob.ts
1121
+ import { join } from "path";
1122
+ import fg from "fast-glob";
1123
+
1124
+ // src/tools/safepath.ts
1125
+ import { realpathSync } from "fs";
1126
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
1127
+ function canonicalize(target) {
1128
+ try {
1129
+ return realpathSync(target);
1130
+ } catch {
1131
+ let current = target;
1132
+ const trailing = [];
1133
+ for (let i = 0; i < 64; i++) {
1134
+ try {
1135
+ return resolve(realpathSync(current), ...trailing);
1136
+ } catch {
1137
+ const parent = dirname(current);
1138
+ if (parent === current) break;
1139
+ trailing.unshift(basename(current));
1140
+ current = parent;
1141
+ }
1142
+ }
1143
+ return resolve(current, ...trailing);
1144
+ }
1145
+ }
1146
+ function resolveInside(cwd, userPath) {
1147
+ const root = canonicalize(resolve(cwd));
1148
+ const target = canonicalize(resolve(root, userPath));
1149
+ const rel = relative(root, target);
1150
+ if (rel !== "" && (isAbsolute(rel) || rel.split(sep)[0] === "..")) {
1151
+ return { ok: false, reason: `Path ${userPath} resolves outside the workspace root ${root}` };
1152
+ }
1153
+ return { ok: true, path: target, relative: rel };
1154
+ }
1155
+ function patternEscapes(pattern) {
1156
+ return isAbsolute(pattern) || pattern.split("/").includes("..") || pattern.split("\\").includes("..");
1157
+ }
1158
+
1159
+ // src/tools/glob.ts
1160
+ var MAX_RESULTS = 500;
1161
+ var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
1162
+ var globTool = {
1163
+ name: "glob",
1164
+ description: 'Find files by glob pattern, most recently modified first. Supports patterns like "src/**/*.ts". Hidden files, node_modules, .git and dist are skipped.',
1165
+ inputSchema: {
1166
+ type: "object",
1167
+ properties: {
1168
+ pattern: { type: "string", description: "Glob pattern, e.g. **/*.test.ts" },
1169
+ path: { type: "string", description: "Directory to search in, relative to the workspace root" }
1170
+ },
1171
+ required: ["pattern"],
1172
+ additionalProperties: false
1173
+ },
1174
+ defaultPermission: "allow",
1175
+ readOnly: true,
1176
+ summarize(input) {
1177
+ return `glob(${brief(input.pattern)})`;
1178
+ },
1179
+ async execute(input, ctx) {
1180
+ const { pattern, path: path15 = "." } = input;
1181
+ if (patternEscapes(pattern)) {
1182
+ return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
1183
+ }
1184
+ const safe = resolveInside(ctx.cwd, path15);
1185
+ if (!safe.ok) return { content: safe.reason, isError: true };
1186
+ const entries = await fg(pattern, {
1187
+ cwd: safe.path,
1188
+ dot: false,
1189
+ onlyFiles: true,
1190
+ followSymbolicLinks: false,
1191
+ suppressErrors: true,
1192
+ ignore: IGNORED_DIRECTORIES,
1193
+ stats: true
1194
+ });
1195
+ if (entries.length === 0) return { content: `No files matched ${pattern}` };
1196
+ entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
1197
+ const paths = entries.slice(0, MAX_RESULTS).map((entry) => join(safe.relative, entry.path));
1198
+ if (entries.length > MAX_RESULTS) {
1199
+ paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
1200
+ }
1201
+ return { content: paths.join("\n") };
1202
+ }
1203
+ };
1204
+
1205
+ // src/ui/workspace-files.ts
1206
+ var MAX_SCANNED = 2e4;
1207
+ var MAX_RESULTS2 = 12;
1208
+ var CACHE_TTL_MS = 1e4;
1209
+ var cached;
1210
+ var inFlight;
1211
+ async function searchWorkspaceFiles(cwd, query) {
1212
+ const files = await workspaceFiles(cwd);
1213
+ const needle = query.trim().toLowerCase();
1214
+ if (needle === "") return files.slice(0, MAX_RESULTS2);
1215
+ const exact = [];
1216
+ const loose = [];
1217
+ for (const file of files) {
1218
+ const haystack = file.toLowerCase();
1219
+ if (haystack.includes(needle)) exact.push(file);
1220
+ else if (isSubsequence(needle, haystack)) loose.push(file);
1221
+ if (exact.length >= MAX_RESULTS2) break;
1222
+ }
1223
+ exact.sort((a, b) => score(a, needle) - score(b, needle));
1224
+ return [...exact, ...loose].slice(0, MAX_RESULTS2);
1225
+ }
1226
+ function workspaceFiles(cwd) {
1227
+ const fresh = cached && cached.cwd === cwd && Date.now() - cached.at < CACHE_TTL_MS;
1228
+ if (fresh) return Promise.resolve(cached.files);
1229
+ if (inFlight) return inFlight;
1230
+ inFlight = fg2("**/*", {
1231
+ cwd,
1232
+ dot: false,
1233
+ onlyFiles: true,
1234
+ followSymbolicLinks: false,
1235
+ suppressErrors: true,
1236
+ ignore: IGNORED_DIRECTORIES
1237
+ }).then((entries) => {
1238
+ const files = entries.slice(0, MAX_SCANNED).sort((a, b) => a.length - b.length);
1239
+ cached = { cwd, files, at: Date.now() };
1240
+ return files;
1241
+ }).catch(() => []).finally(() => {
1242
+ inFlight = void 0;
1243
+ });
1244
+ return inFlight;
1245
+ }
1246
+ function score(file, needle) {
1247
+ const basename3 = file.slice(file.lastIndexOf("/") + 1).toLowerCase();
1248
+ if (basename3.startsWith(needle)) return 0;
1249
+ if (basename3.includes(needle)) return 1;
1250
+ return 2;
1251
+ }
1252
+ function isSubsequence(needle, haystack) {
1253
+ let index = 0;
1254
+ for (const character of haystack) {
1255
+ if (character === needle[index]) index += 1;
1256
+ if (index === needle.length) return true;
1257
+ }
1258
+ return needle.length === 0;
1259
+ }
1260
+
1261
+ // src/tools/jobs.ts
1262
+ import { spawn } from "child_process";
1263
+ var MAX_JOBS = 8;
1264
+ var MAX_JOB_OUTPUT = 1e5;
1265
+ var jobs = /* @__PURE__ */ new Map();
1266
+ var counter = 0;
1267
+ function startJob(command, cwd) {
1268
+ const live = [...jobs.values()].filter((job2) => job2.state === "running");
1269
+ if (live.length >= MAX_JOBS) {
1270
+ return { error: `At most ${MAX_JOBS} background commands can run at once.` };
1271
+ }
1272
+ counter += 1;
1273
+ const id = `bg-${counter}`;
1274
+ const child = spawn(command, {
1275
+ shell: true,
1276
+ cwd,
1277
+ detached: process.platform !== "win32",
1278
+ stdio: ["ignore", "pipe", "pipe"],
1279
+ windowsHide: true
1280
+ });
1281
+ const job = {
1282
+ id,
1283
+ command,
1284
+ state: "running",
1285
+ exitCode: null,
1286
+ signal: null,
1287
+ child,
1288
+ buffer: "",
1289
+ reported: 0,
1290
+ truncated: false
1291
+ };
1292
+ jobs.set(id, job);
1293
+ const collect = (chunk) => {
1294
+ const text = chunk.toString("utf8");
1295
+ const room = MAX_JOB_OUTPUT - job.buffer.length;
1296
+ if (text.length <= room) {
1297
+ job.buffer += text;
1298
+ return;
1299
+ }
1300
+ job.truncated = true;
1301
+ job.buffer = (job.buffer + text).slice(-MAX_JOB_OUTPUT);
1302
+ job.reported = Math.min(job.reported, job.buffer.length);
1303
+ };
1304
+ child.stdout?.on("data", collect);
1305
+ child.stderr?.on("data", collect);
1306
+ child.on("error", (error) => {
1307
+ job.buffer += `
1308
+ Command failed to start: ${error.message}`;
1309
+ job.state = "exited";
1310
+ job.exitCode = job.exitCode ?? 1;
1311
+ });
1312
+ child.on("close", (code, signal) => {
1313
+ if (job.state !== "killed") job.state = "exited";
1314
+ job.exitCode = code;
1315
+ job.signal = signal;
1316
+ });
1317
+ return { id };
1318
+ }
1319
+ function readJob(id) {
1320
+ const job = jobs.get(id);
1321
+ if (!job) return void 0;
1322
+ const output = job.buffer.slice(job.reported);
1323
+ job.reported = job.buffer.length;
1324
+ return { ...summary(job), output, truncated: job.truncated };
1325
+ }
1326
+ function killJob(id) {
1327
+ const job = jobs.get(id);
1328
+ if (!job) return false;
1329
+ if (job.state === "running") {
1330
+ job.state = "killed";
1331
+ killTree(job.child);
1332
+ }
1333
+ return true;
1334
+ }
1335
+ function listJobs() {
1336
+ return [...jobs.values()].map(summary);
1337
+ }
1338
+ function killAllJobs() {
1339
+ for (const job of jobs.values()) {
1340
+ if (job.state === "running") {
1341
+ job.state = "killed";
1342
+ killTree(job.child);
1343
+ }
1344
+ }
1345
+ }
1346
+ function summary(job) {
1347
+ return {
1348
+ id: job.id,
1349
+ command: job.command,
1350
+ state: job.state,
1351
+ exitCode: job.exitCode,
1352
+ signal: job.signal
1353
+ };
1354
+ }
1355
+ function killTree(child) {
1356
+ if (child.pid == null) return;
1357
+ if (process.platform === "win32") {
1358
+ try {
1359
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
1360
+ } catch {
1361
+ child.kill("SIGKILL");
1362
+ }
1363
+ return;
1364
+ }
1365
+ try {
1366
+ process.kill(-child.pid, "SIGKILL");
1367
+ } catch {
1368
+ child.kill("SIGKILL");
1369
+ }
1370
+ }
1371
+
1090
1372
  // src/core/memory.ts
1091
1373
  import { createHash } from "crypto";
1092
1374
  import { readFile as readFile3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
@@ -1530,8 +1812,23 @@ var MAX_PAUSE_RESUMES = 5;
1530
1812
  var MAX_RETRIES2 = 3;
1531
1813
  var INITIAL_BACKOFF_MS = 1e3;
1532
1814
  var MAX_TURN_STEPS = 64;
1533
- var MAX_TOOL_CALLS_PER_STEP = 32;
1815
+ function createExecutionLane() {
1816
+ let tail2 = Promise.resolve();
1817
+ return {
1818
+ run(operation) {
1819
+ const result = tail2.then(operation);
1820
+ tail2 = result.then(() => void 0, () => void 0);
1821
+ return result;
1822
+ }
1823
+ };
1824
+ }
1534
1825
  async function runTurn(cfg, history, hooks, signal) {
1826
+ const permissionLane = cfg.permissionLane ?? createExecutionLane();
1827
+ const originalHooks = hooks;
1828
+ hooks = {
1829
+ ...hooks,
1830
+ requestPermission: (request) => permissionLane.run(async () => signal.aborted ? "deny" : originalHooks.requestPermission(request))
1831
+ };
1535
1832
  let messages = [...history];
1536
1833
  let pauses = 0;
1537
1834
  let steps = 0;
@@ -1549,40 +1846,35 @@ async function runTurn(cfg, history, hooks, signal) {
1549
1846
  hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1550
1847
  return messages;
1551
1848
  }
1552
- const budgetDecision = cfg.budget?.beforeRequest({
1553
- modelRef: cfg.modelRef,
1554
- maxOutputTokens: cfg.maxTokens,
1555
- estimatedInputTokens: estimateRequestTokens(cfg, messages)
1556
- });
1557
- if (budgetDecision && !budgetDecision.allowed) {
1558
- hooks.onEvent({ type: "notice", level: "warn", text: budgetDecision.reason });
1559
- hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1560
- return messages;
1561
- }
1562
- hooks.onEvent({ type: "turn_start" });
1563
- const outcome = await consumeStream(
1564
- cfg,
1565
- messages,
1566
- hooks,
1567
- signal,
1568
- budgetDecision?.maxOutputTokens ?? cfg.maxTokens
1569
- );
1849
+ const request = async () => {
1850
+ if (signal.aborted) return { content: [], stopReason: "aborted" };
1851
+ const budgetDecision = cfg.budget?.beforeRequest({
1852
+ modelRef: cfg.modelRef,
1853
+ maxOutputTokens: cfg.maxTokens,
1854
+ estimatedInputTokens: estimateRequestTokens(cfg, messages)
1855
+ });
1856
+ if (budgetDecision && !budgetDecision.allowed) {
1857
+ hooks.onEvent({ type: "notice", level: "warn", text: budgetDecision.reason });
1858
+ hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1859
+ return null;
1860
+ }
1861
+ hooks.onEvent({ type: "turn_start" });
1862
+ return consumeStream(
1863
+ cfg,
1864
+ messages,
1865
+ hooks,
1866
+ signal,
1867
+ budgetDecision?.maxOutputTokens ?? cfg.maxTokens
1868
+ );
1869
+ };
1870
+ const outcome = await (cfg.modelRequestLane?.run(request) ?? request());
1871
+ if (!outcome) return messages;
1570
1872
  messages.push({ role: "assistant", content: outcome.content });
1571
1873
  if (outcome.stopReason === "aborted") {
1572
1874
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1573
1875
  return sanitizeHistory(messages);
1574
1876
  }
1575
1877
  if (outcome.stopReason === "tool_use") {
1576
- const callCount = outcome.content.filter((block) => block.type === "tool_use").length;
1577
- if (callCount > MAX_TOOL_CALLS_PER_STEP) {
1578
- hooks.onEvent({
1579
- type: "notice",
1580
- level: "warn",
1581
- text: `The model returned ${callCount} tool calls at once; the limit is ${MAX_TOOL_CALLS_PER_STEP}. None were run. Ask it to split the work into smaller batches.`
1582
- });
1583
- hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1584
- return sanitizeHistory(messages);
1585
- }
1586
1878
  const results = await runToolCalls(cfg, outcome.content, hooks, signal);
1587
1879
  if (results.length > 0) messages.push({ role: "user", content: results });
1588
1880
  if (signal.aborted) {
@@ -1770,15 +2062,37 @@ function estimateRequestTokens(cfg, messages) {
1770
2062
  async function runToolCalls(cfg, content, hooks, signal) {
1771
2063
  const calls = content.filter((block) => block.type === "tool_use");
1772
2064
  const results = [];
1773
- for (const call of calls) results.push(await runOneCall(cfg, call, hooks, signal));
2065
+ const run = (call) => {
2066
+ const operation = () => runOneCall(cfg, call, hooks, signal);
2067
+ return call.name !== "task" && cfg.executionLane ? cfg.executionLane.run(operation) : operation();
2068
+ };
2069
+ for (let index = 0; index < calls.length; ) {
2070
+ const call = calls[index];
2071
+ if (call.name !== "task") {
2072
+ results.push(await run(call));
2073
+ index += 1;
2074
+ continue;
2075
+ }
2076
+ const batch = [];
2077
+ while (calls[index]?.name === "task") {
2078
+ batch.push(run(calls[index]));
2079
+ index += 1;
2080
+ }
2081
+ const settled = await Promise.allSettled(batch);
2082
+ for (const result of settled) {
2083
+ if (result.status === "rejected") throw result.reason;
2084
+ results.push(result.value);
2085
+ }
2086
+ }
1774
2087
  return results;
1775
2088
  }
1776
2089
  async function runOneCall(cfg, call, hooks, signal) {
2090
+ if (signal.aborted) return toolError(call.id, "Interrupted by the user before this tool ran.");
1777
2091
  const tool = cfg.tools.get(call.name);
1778
2092
  if (!tool) {
1779
2093
  return toolError(call.id, `Unknown tool "${call.name}". Use only the tools provided.`);
1780
2094
  }
1781
- const summary = describeCall(tool, call.input);
2095
+ const summary2 = describeCall(tool, call.input);
1782
2096
  const ctx = {
1783
2097
  cwd: cfg.cwd,
1784
2098
  signal,
@@ -1792,30 +2106,35 @@ async function runOneCall(cfg, call, hooks, signal) {
1792
2106
  }) !== "deny"
1793
2107
  };
1794
2108
  const requestedPermission = requestedPermissionFor(tool, call.input, ctx);
1795
- const gate = cfg.permissions.decide(tool, requestedPermission);
1796
- if (gate === "deny") {
1797
- return toolError(
1798
- call.id,
1799
- cfg.permissions.denyReason?.(tool, requestedPermission) ?? `Tool "${tool.name}" is disabled by the user's configuration. Do not retry it; find another way or ask.`
1800
- );
1801
- }
1802
- if (gate === "ask") {
1803
- const decision = await hooks.requestPermission({
1804
- toolName: tool.name,
1805
- summary,
1806
- input: call.input,
1807
- display: await previewCall(tool, call.input, ctx),
1808
- allowAlways: canAlwaysApprove(tool)
1809
- });
1810
- if (decision === "deny") {
1811
- return toolError(call.id, "The user declined this call. Do not retry it; ask how to proceed.");
2109
+ const permission = async () => {
2110
+ const gate = cfg.permissions.decide(tool, requestedPermission);
2111
+ if (gate === "deny") {
2112
+ return toolError(
2113
+ call.id,
2114
+ cfg.permissions.denyReason?.(tool, requestedPermission) ?? `Tool "${tool.name}" is disabled by the user's configuration. Do not retry it; find another way or ask.`
2115
+ );
1812
2116
  }
1813
- if (decision === "always") cfg.permissions.grantForSession(tool.name);
1814
- }
2117
+ if (gate === "ask") {
2118
+ const decision = await hooks.requestPermission({
2119
+ toolName: tool.name,
2120
+ summary: summary2,
2121
+ input: call.input,
2122
+ display: await previewCall(tool, call.input, ctx),
2123
+ allowAlways: canAlwaysApprove(tool)
2124
+ });
2125
+ if (decision === "deny") {
2126
+ return toolError(call.id, "The user declined this call. Do not retry it; ask how to proceed.");
2127
+ }
2128
+ if (decision === "always") cfg.permissions.grantForSession(tool.name);
2129
+ }
2130
+ return void 0;
2131
+ };
2132
+ const permissionResult = await permission();
2133
+ if (permissionResult) return permissionResult;
1815
2134
  if (signal.aborted) {
1816
2135
  return toolError(call.id, "Interrupted by the user before this tool ran.");
1817
2136
  }
1818
- hooks.onEvent({ type: "tool_start", id: call.id, name: tool.name, summary });
2137
+ hooks.onEvent({ type: "tool_start", id: call.id, name: tool.name, summary: summary2 });
1819
2138
  try {
1820
2139
  const result = await tool.execute(call.input, ctx);
1821
2140
  hooks.onEvent({
@@ -1907,6 +2226,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1907
2226
  costUsd
1908
2227
  });
1909
2228
  return {
2229
+ hasLimits: limits.maxTokensPerTurn > 0 || limits.maxCostUsdPerTurn > 0,
1910
2230
  beforeRequest(request) {
1911
2231
  const current = snapshot();
1912
2232
  if (limits.maxTokensPerTurn > 0 && current.tokens >= limits.maxTokensPerTurn) {
@@ -1987,43 +2307,6 @@ import {
1987
2307
  writeFile as writeFile5
1988
2308
  } from "fs/promises";
1989
2309
  import path6 from "path";
1990
-
1991
- // src/tools/safepath.ts
1992
- import { realpathSync } from "fs";
1993
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
1994
- function canonicalize(target) {
1995
- try {
1996
- return realpathSync(target);
1997
- } catch {
1998
- let current = target;
1999
- const trailing = [];
2000
- for (let i = 0; i < 64; i++) {
2001
- try {
2002
- return resolve(realpathSync(current), ...trailing);
2003
- } catch {
2004
- const parent = dirname(current);
2005
- if (parent === current) break;
2006
- trailing.unshift(basename(current));
2007
- current = parent;
2008
- }
2009
- }
2010
- return resolve(current, ...trailing);
2011
- }
2012
- }
2013
- function resolveInside(cwd, userPath) {
2014
- const root = canonicalize(resolve(cwd));
2015
- const target = canonicalize(resolve(root, userPath));
2016
- const rel = relative(root, target);
2017
- if (rel !== "" && (isAbsolute(rel) || rel.split(sep)[0] === "..")) {
2018
- return { ok: false, reason: `Path ${userPath} resolves outside the workspace root ${root}` };
2019
- }
2020
- return { ok: true, path: target, relative: rel };
2021
- }
2022
- function patternEscapes(pattern) {
2023
- return isAbsolute(pattern) || pattern.split("/").includes("..") || pattern.split("\\").includes("..");
2024
- }
2025
-
2026
- // src/core/checkpoint.ts
2027
2310
  var VERSION = 1;
2028
2311
  var MAX_FILE_BYTES = 5e6;
2029
2312
  var MAX_CHECKPOINT_BYTES = 2e7;
@@ -2335,14 +2618,14 @@ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2335
2618
  import path7 from "path";
2336
2619
 
2337
2620
  // src/tools/bash.ts
2338
- import { spawn } from "child_process";
2621
+ import { spawn as spawn2 } from "child_process";
2339
2622
  var DEFAULT_TIMEOUT = 12e4;
2340
2623
  var MAX_TIMEOUT = 6e5;
2341
2624
  var MAX_OUTPUT = 1e5;
2342
2625
  var MAX_COMMAND_CHARS = 2e4;
2343
2626
  var bashTool = {
2344
2627
  name: "bash",
2345
- description: "Run a shell command in the workspace root. stdout and stderr are merged. Avoid commands that wait for interactive input.",
2628
+ description: "Run a shell command in the workspace root. stdout and stderr are merged. Avoid commands that wait for interactive input. Set background: true to keep a long command (dev server, test watcher, build) running while you carry on with other work, then collect its output with bash_output.",
2346
2629
  inputSchema: {
2347
2630
  type: "object",
2348
2631
  properties: {
@@ -2353,8 +2636,12 @@ var bashTool = {
2353
2636
  },
2354
2637
  timeoutMs: {
2355
2638
  type: "integer",
2356
- description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, maximum ${MAX_TIMEOUT})`,
2639
+ description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, maximum ${MAX_TIMEOUT}); ignored for background commands`,
2357
2640
  minimum: 1
2641
+ },
2642
+ background: {
2643
+ type: "boolean",
2644
+ description: "Start the command and return at once instead of waiting for it. Read its output later with bash_output. Background commands have no timeout and are killed when the session ends."
2358
2645
  }
2359
2646
  },
2360
2647
  required: ["command"],
@@ -2362,13 +2649,14 @@ var bashTool = {
2362
2649
  },
2363
2650
  defaultPermission: "ask",
2364
2651
  summarize(input) {
2365
- return `bash(${brief(input.command)})`;
2652
+ const { command, background } = input;
2653
+ return background ? `bash(${brief(command)}, background)` : `bash(${brief(command)})`;
2366
2654
  },
2367
2655
  async preview(input) {
2368
2656
  return { kind: "text", text: input.command };
2369
2657
  },
2370
2658
  execute(input, ctx) {
2371
- const { command, timeoutMs } = input;
2659
+ const { command, timeoutMs, background } = input;
2372
2660
  if (command.length > MAX_COMMAND_CHARS) {
2373
2661
  return Promise.resolve({
2374
2662
  content: `Command exceeds the ${MAX_COMMAND_CHARS} character limit.`,
@@ -2380,8 +2668,17 @@ var bashTool = {
2380
2668
  if (ctx.signal.aborted) {
2381
2669
  return Promise.resolve({ content: "[cancelled before the command started]", isError: true });
2382
2670
  }
2671
+ if (background) {
2672
+ const started = startJob(command, ctx.cwd);
2673
+ if ("error" in started) {
2674
+ return Promise.resolve({ content: started.error, isError: true });
2675
+ }
2676
+ return Promise.resolve({
2677
+ content: `[started in the background as ${started.id}] Read its output with bash_output({ id: "${started.id}" }).`
2678
+ });
2679
+ }
2383
2680
  return new Promise((settle) => {
2384
- const child = spawn(command, {
2681
+ const child = spawn2(command, {
2385
2682
  shell: true,
2386
2683
  cwd: ctx.cwd,
2387
2684
  detached: process.platform !== "win32",
@@ -2409,11 +2706,11 @@ var bashTool = {
2409
2706
  };
2410
2707
  child.stdout.on("data", collect);
2411
2708
  child.stderr.on("data", collect);
2412
- const killTree = () => {
2709
+ const killTree2 = () => {
2413
2710
  if (child.pid == null) return;
2414
2711
  if (process.platform === "win32") {
2415
2712
  try {
2416
- spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2713
+ spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2417
2714
  } catch {
2418
2715
  child.kill("SIGKILL");
2419
2716
  }
@@ -2429,11 +2726,11 @@ var bashTool = {
2429
2726
  let aborted = false;
2430
2727
  const timer = setTimeout(() => {
2431
2728
  timedOut = true;
2432
- killTree();
2729
+ killTree2();
2433
2730
  }, timeout);
2434
2731
  const onAbort = () => {
2435
2732
  aborted = true;
2436
- killTree();
2733
+ killTree2();
2437
2734
  };
2438
2735
  ctx.signal.addEventListener("abort", onAbort, { once: true });
2439
2736
  const finish = (result) => {
@@ -2495,20 +2792,20 @@ async function detectDiagnosticCommands(cwd, configured = []) {
2495
2792
  }
2496
2793
  async function runAutomaticDiagnostics(options) {
2497
2794
  const commands = await detectDiagnosticCommands(options.cwd, options.configuredCommands);
2498
- const summary = { commands, failed: [], skipped: false };
2795
+ const summary2 = { commands, failed: [], skipped: false };
2499
2796
  if (options.changedFiles.length === 0 || commands.length === 0 || options.signal.aborted) {
2500
- summary.skipped = true;
2501
- return summary;
2797
+ summary2.skipped = true;
2798
+ return summary2;
2502
2799
  }
2503
2800
  const gate = options.permissions.decide(diagnosticsTool);
2504
2801
  if (gate === "deny") {
2505
- summary.skipped = true;
2802
+ summary2.skipped = true;
2506
2803
  options.hooks.onEvent({
2507
2804
  type: "notice",
2508
2805
  level: "warn",
2509
2806
  text: "Automatic project checks were skipped by the current permission settings."
2510
2807
  });
2511
- return summary;
2808
+ return summary2;
2512
2809
  }
2513
2810
  if (gate === "ask") {
2514
2811
  const decision = await options.hooks.requestPermission({
@@ -2523,17 +2820,17 @@ async function runAutomaticDiagnostics(options) {
2523
2820
  allowAlways: true
2524
2821
  });
2525
2822
  if (options.signal.aborted) {
2526
- summary.skipped = true;
2527
- return summary;
2823
+ summary2.skipped = true;
2824
+ return summary2;
2528
2825
  }
2529
2826
  if (decision === "deny") {
2530
- summary.skipped = true;
2827
+ summary2.skipped = true;
2531
2828
  options.hooks.onEvent({
2532
2829
  type: "notice",
2533
2830
  level: "warn",
2534
2831
  text: "Automatic project checks were skipped."
2535
2832
  });
2536
- return summary;
2833
+ return summary2;
2537
2834
  }
2538
2835
  if (decision === "always") options.permissions.grantForSession(diagnosticsTool.name);
2539
2836
  }
@@ -2546,7 +2843,7 @@ async function runAutomaticDiagnostics(options) {
2546
2843
  const execute = options.execute ?? ((command, context) => bashTool.execute({ command, timeoutMs: DIAGNOSTIC_TIMEOUT_MS }, context));
2547
2844
  for (const [index, command] of commands.entries()) {
2548
2845
  if (options.signal.aborted) {
2549
- summary.skipped = true;
2846
+ summary2.skipped = true;
2550
2847
  break;
2551
2848
  }
2552
2849
  const id = `diagnostics-${Date.now()}-${index}`;
@@ -2554,7 +2851,7 @@ async function runAutomaticDiagnostics(options) {
2554
2851
  options.hooks.onEvent({ type: "tool_start", id, name: diagnosticsTool.name, summary: callSummary });
2555
2852
  try {
2556
2853
  const result = await execute(command, ctx);
2557
- if (result.isError) summary.failed.push(command);
2854
+ if (result.isError) summary2.failed.push(command);
2558
2855
  options.hooks.onEvent({
2559
2856
  type: "tool_end",
2560
2857
  id,
@@ -2563,7 +2860,7 @@ async function runAutomaticDiagnostics(options) {
2563
2860
  display: result.display
2564
2861
  });
2565
2862
  } catch (error) {
2566
- summary.failed.push(command);
2863
+ summary2.failed.push(command);
2567
2864
  options.hooks.onEvent({
2568
2865
  type: "tool_end",
2569
2866
  id,
@@ -2572,7 +2869,7 @@ async function runAutomaticDiagnostics(options) {
2572
2869
  });
2573
2870
  }
2574
2871
  }
2575
- return summary;
2872
+ return summary2;
2576
2873
  }
2577
2874
  async function readPackageJson(file) {
2578
2875
  try {
@@ -2991,7 +3288,7 @@ async function compactHistory(options) {
2991
3288
  if (!rendered.trim()) {
2992
3289
  return { history: options.history, compacted: false, removedMessages: 0 };
2993
3290
  }
2994
- let summary;
3291
+ let summary2;
2995
3292
  let usage;
2996
3293
  let rateLimits;
2997
3294
  if (rendered.length <= SINGLE_REQUEST_MAX_CHARS) {
@@ -3005,7 +3302,7 @@ async function compactHistory(options) {
3005
3302
  SYSTEM_SUMMARIZE,
3006
3303
  requestMax
3007
3304
  );
3008
- summary = result.text;
3305
+ summary2 = result.text;
3009
3306
  usage = result.usage;
3010
3307
  rateLimits = result.rateLimits;
3011
3308
  } else {
@@ -3040,19 +3337,19 @@ async function compactHistory(options) {
3040
3337
  chunkResults.map((result) => result.text),
3041
3338
  limits.merge
3042
3339
  );
3043
- summary = mergeResult.text;
3340
+ summary2 = mergeResult.text;
3044
3341
  if (mergeResult.usage) {
3045
3342
  usage = usage ? mergeUsage([usage, mergeResult.usage]) : mergeResult.usage;
3046
3343
  }
3047
3344
  if (mergeResult.rateLimits) rateLimits = mergeResult.rateLimits;
3048
3345
  }
3049
- summary = summary.trim();
3050
- if (!summary) throw new Error("The provider returned an empty context summary.");
3346
+ summary2 = summary2.trim();
3347
+ if (!summary2) throw new Error("The provider returned an empty context summary.");
3051
3348
  const prefix = [
3052
3349
  {
3053
3350
  role: "user",
3054
3351
  content: [{ type: "text", text: `[Earlier conversation summary]
3055
- ${summary}` }]
3352
+ ${summary2}` }]
3056
3353
  },
3057
3354
  {
3058
3355
  role: "assistant",
@@ -3196,8 +3493,8 @@ async function mergePartialSummaries(provider, model, maxTokens, signal, summari
3196
3493
  while (current.length > 1) {
3197
3494
  const groups = groupSummaries(current, MERGE_TARGET_CHARS);
3198
3495
  const sources = groups.map(
3199
- (group) => group.map((summary, index) => `## Part ${index + 1}
3200
- ${summary}`).join("\n\n")
3496
+ (group) => group.map((summary2, index) => `## Part ${index + 1}
3497
+ ${summary2}`).join("\n\n")
3201
3498
  );
3202
3499
  const merged = await summarizeSourcesParallel(
3203
3500
  provider,
@@ -3256,22 +3553,22 @@ function groupSummaries(summaries, targetChars) {
3256
3553
  const groups = [];
3257
3554
  let current = [];
3258
3555
  let size = 0;
3259
- for (const summary of summaries) {
3260
- if (current.length >= 2 && size + 12 + summary.length > targetChars) {
3556
+ for (const summary2 of summaries) {
3557
+ if (current.length >= 2 && size + 12 + summary2.length > targetChars) {
3261
3558
  groups.push(current);
3262
3559
  current = [];
3263
3560
  size = 0;
3264
3561
  }
3265
3562
  const separator = current.length > 0 ? 12 : 0;
3266
- current.push(summary);
3267
- size += separator + summary.length;
3563
+ current.push(summary2);
3564
+ size += separator + summary2.length;
3268
3565
  }
3269
3566
  if (current.length > 0) groups.push(current);
3270
3567
  return groups;
3271
3568
  }
3272
3569
  async function summarizeText(provider, model, maxTokens, signal, source, systemPrompt, maxOutputTokens) {
3273
3570
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3274
- let summary = "";
3571
+ let summary2 = "";
3275
3572
  let finalText2 = "";
3276
3573
  let usage;
3277
3574
  let rateLimits;
@@ -3285,7 +3582,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
3285
3582
  signal
3286
3583
  });
3287
3584
  for await (const event of stream) {
3288
- if (event.type === "text_delta") summary += event.text;
3585
+ if (event.type === "text_delta") summary2 += event.text;
3289
3586
  else if (event.type === "usage") usage = event.usage;
3290
3587
  else if (event.type === "rate_limits") rateLimits = event.limits;
3291
3588
  else if (event.type === "done") {
@@ -3293,7 +3590,7 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
3293
3590
  }
3294
3591
  }
3295
3592
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
3296
- return { text: (finalText2 || summary).trim(), usage, rateLimits };
3593
+ return { text: (finalText2 || summary2).trim(), usage, rateLimits };
3297
3594
  }
3298
3595
  function mergeUsage(usages) {
3299
3596
  return usages.reduce(
@@ -3406,6 +3703,9 @@ var SUBAGENT_SYSTEM = [
3406
3703
  'Your final message is the return value \u2014 it is the only thing the caller ever sees. End with the answer itself: the findings, the file paths, the conclusion. Never end with "I will now..." or a recap of the steps you took. Carry over every concrete detail the caller needs (paths, names, line numbers) and leave out the narration of how you found them.'
3407
3704
  ].join("\n");
3408
3705
  function createSubagentRunner(makeConfig, tools) {
3706
+ const permissionLane = createExecutionLane();
3707
+ const executionLane = createExecutionLane();
3708
+ const modelRequestLane = createExecutionLane();
3409
3709
  return {
3410
3710
  async run(request) {
3411
3711
  if (request.signal.aborted) return SUBAGENT_CANCELLED;
@@ -3413,6 +3713,9 @@ function createSubagentRunner(makeConfig, tools) {
3413
3713
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
3414
3714
  const cfg = {
3415
3715
  ...base,
3716
+ permissionLane,
3717
+ executionLane,
3718
+ modelRequestLane: base.budget?.hasLimits ? modelRequestLane : void 0,
3416
3719
  provider: {
3417
3720
  id: base.provider.id,
3418
3721
  kind: base.provider.kind,
@@ -3637,7 +3940,7 @@ function clip(value) {
3637
3940
  // package.json
3638
3941
  var package_default = {
3639
3942
  name: "@kernelonpanic/kitcode",
3640
- version: "1.3.0",
3943
+ version: "1.3.2",
3641
3944
  description: "Terminal coding agent with a config you never have to write by hand",
3642
3945
  type: "module",
3643
3946
  license: "MIT",
@@ -3705,7 +4008,7 @@ var package_default = {
3705
4008
 
3706
4009
  // src/version.ts
3707
4010
  var KITCODE_VERSION = package_default.version;
3708
- var KITCODE_COMMIT = true ? "7cd983ca3dc89406326e5e513870686036c3ab9c" : "development";
4011
+ var KITCODE_COMMIT = true ? "993d55335487da4915fa49660911ba6e69aa3685" : "development";
3709
4012
 
3710
4013
  // src/mcp/client.ts
3711
4014
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3903,8 +4206,8 @@ function describeFailure2(error, stderr, secrets) {
3903
4206
  return redactSecrets(stderr ? `${message} \u2014 ${stderr}` : message, secrets);
3904
4207
  }
3905
4208
  function configuredSecrets(config) {
3906
- const record2 = config.type === "stdio" ? config.env : config.headers;
3907
- return Object.values(expandEnvRecord(record2)).filter((value) => value.length >= 4);
4209
+ const record3 = config.type === "stdio" ? config.env : config.headers;
4210
+ return Object.values(expandEnvRecord(record3)).filter((value) => value.length >= 4);
3908
4211
  }
3909
4212
  function inheritedEnv(source = process.env) {
3910
4213
  const env = {};
@@ -4000,10 +4303,10 @@ function endpoint(origin, path15, parse2) {
4000
4303
  return { url: `${origin}${path15}`, parse: parse2 };
4001
4304
  }
4002
4305
  function deepSeekBalance(body) {
4003
- const infos = record(body)?.["balance_infos"];
4306
+ const infos = record2(body)?.["balance_infos"];
4004
4307
  if (!Array.isArray(infos)) return null;
4005
4308
  const balances = infos.flatMap((item) => {
4006
- const entry = record(item);
4309
+ const entry = record2(item);
4007
4310
  const currency = currencyCode(entry?.["currency"]);
4008
4311
  const amount = decimal(entry?.["total_balance"]);
4009
4312
  return currency && amount ? [{ kind: "balance", currency, amount }] : [];
@@ -4011,11 +4314,11 @@ function deepSeekBalance(body) {
4011
4314
  return balances.length > 0 ? balances : null;
4012
4315
  }
4013
4316
  function openRouterKeyBalance(body) {
4014
- const remaining = finiteNumber(record(record(body)?.["data"])?.["limit_remaining"]);
4317
+ const remaining = finiteNumber(record2(record2(body)?.["data"])?.["limit_remaining"]);
4015
4318
  return remaining === null ? null : [{ kind: "key-limit", currency: "USD", amount: String(remaining) }];
4016
4319
  }
4017
4320
  function openRouterCreditsBalance(body) {
4018
- const data = record(record(body)?.["data"]);
4321
+ const data = record2(record2(body)?.["data"]);
4019
4322
  const credits = finiteNumber(data?.["total_credits"]);
4020
4323
  const usage = finiteNumber(data?.["total_usage"]);
4021
4324
  if (credits === null || usage === null) return null;
@@ -4028,26 +4331,26 @@ function openRouterCreditsBalance(body) {
4028
4331
  ];
4029
4332
  }
4030
4333
  function siliconFlowBalance(body) {
4031
- const root = record(body);
4334
+ const root = record2(body);
4032
4335
  if (root?.["status"] !== true) return null;
4033
- const amount = decimal(record(root["data"])?.["balance"]);
4336
+ const amount = decimal(record2(root["data"])?.["balance"]);
4034
4337
  return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
4035
4338
  }
4036
4339
  function apiMartBalance(body) {
4037
- const root = record(body);
4340
+ const root = record2(body);
4038
4341
  if (root?.["success"] !== true) return null;
4039
4342
  const amount = decimal(root["remain_balance"]);
4040
4343
  return amount ? [{ kind: "balance", currency: "credits", amount }] : null;
4041
4344
  }
4042
4345
  function genericBalance(body) {
4043
- const root = record(body);
4044
- const data = record(root?.["data"]);
4346
+ const root = record2(body);
4347
+ const data = record2(root?.["data"]);
4045
4348
  const amount = decimal(root?.["balance"] ?? data?.["balance"]);
4046
4349
  if (!amount) return null;
4047
4350
  const currency = currencyCode(root?.["currency"] ?? data?.["currency"]) ?? "credits";
4048
4351
  return [{ kind: "balance", currency, amount }];
4049
4352
  }
4050
- function record(value) {
4353
+ function record2(value) {
4051
4354
  return typeof value === "object" && value !== null ? value : null;
4052
4355
  }
4053
4356
  function currencyCode(value) {
@@ -4075,8 +4378,8 @@ async function limitedResponseText(response, limit) {
4075
4378
  let text = "";
4076
4379
  try {
4077
4380
  while (true) {
4078
- const { done, value } = await reader.read();
4079
- if (done) return text + decoder.decode();
4381
+ const { done: done2, value } = await reader.read();
4382
+ if (done2) return text + decoder.decode();
4080
4383
  size += value.byteLength;
4081
4384
  if (size > limit) {
4082
4385
  await reader.cancel();
@@ -4096,15 +4399,15 @@ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4096
4399
  var CACHE_VERSION = 3;
4097
4400
  async function loadModels(provider, refresh = false) {
4098
4401
  const file = cacheFile(provider.id);
4099
- const cached = await readCache(file);
4100
- if (!refresh && cached?.version === CACHE_VERSION && Date.now() - cached.fetchedAt < MAX_AGE_MS) {
4101
- return cached.models;
4402
+ const cached2 = await readCache(file);
4403
+ if (!refresh && cached2?.version === CACHE_VERSION && Date.now() - cached2.fetchedAt < MAX_AGE_MS) {
4404
+ return cached2.models;
4102
4405
  }
4103
4406
  let models;
4104
4407
  try {
4105
4408
  models = await provider.listModels();
4106
4409
  } catch (error) {
4107
- if (cached) return cached.models;
4410
+ if (cached2) return cached2.models;
4108
4411
  const known = provider.knownModels();
4109
4412
  if (known.length > 0) return known;
4110
4413
  throw error;
@@ -4112,6 +4415,9 @@ async function loadModels(provider, refresh = false) {
4112
4415
  await writeCache(file, models);
4113
4416
  return models;
4114
4417
  }
4418
+ function refreshModels(provider) {
4419
+ return loadModels(provider, true);
4420
+ }
4115
4421
  function cacheFile(providerId) {
4116
4422
  return path9.join(cacheDir, "models", `${providerId.replace(/[^\w.-]/g, "_")}.json`);
4117
4423
  }
@@ -4696,8 +5002,8 @@ function normalizeOpenAiUsage(usage) {
4696
5002
  function createRegistry(config, auth) {
4697
5003
  const built = /* @__PURE__ */ new Map();
4698
5004
  function get(id) {
4699
- const cached = built.get(id);
4700
- if (cached) return cached;
5005
+ const cached2 = built.get(id);
5006
+ if (cached2) return cached2;
4701
5007
  const entry = config.providers[id];
4702
5008
  if (!entry) throw new Error(unknownProviderMessage(id, Object.keys(config.providers)));
4703
5009
  const provider = build(id, entry, apiKey(id, entry, auth));
@@ -4789,13 +5095,66 @@ function isFileEdit(tool) {
4789
5095
  return tool.name === "write" || tool.name === "edit";
4790
5096
  }
4791
5097
 
5098
+ // src/tools/bash-output.ts
5099
+ var bashOutputTool = {
5100
+ name: "bash_output",
5101
+ description: "Collect output from commands started with bash({ background: true }). Returns only the output that arrived since the last read. Omit id to list every background command. Set kill to stop one.",
5102
+ inputSchema: {
5103
+ type: "object",
5104
+ properties: {
5105
+ id: { type: "string", description: 'Background command id, e.g. "bg-1"' },
5106
+ kill: { type: "boolean", description: "Stop the command instead of reading it" }
5107
+ },
5108
+ additionalProperties: false
5109
+ },
5110
+ defaultPermission: "allow",
5111
+ readOnly: true,
5112
+ summarize(input) {
5113
+ const { id, kill } = input;
5114
+ if (!id) return "bash_output(list)";
5115
+ return kill ? `bash_output(${id}, kill)` : `bash_output(${id})`;
5116
+ },
5117
+ execute(input) {
5118
+ const { id, kill } = input;
5119
+ if (!id) {
5120
+ const jobs2 = listJobs();
5121
+ if (jobs2.length === 0) return done({ content: "No background commands have been started." });
5122
+ const lines = jobs2.map((job2) => `${job2.id} ${describeState(job2.state, job2.exitCode, job2.signal)} ${job2.command}`);
5123
+ return done({ content: lines.join("\n") });
5124
+ }
5125
+ if (kill) {
5126
+ const stopped = killJob(id);
5127
+ return done(
5128
+ stopped ? { content: `[${id} stopped]` } : { content: `No background command with id "${id}".`, isError: true }
5129
+ );
5130
+ }
5131
+ const job = readJob(id);
5132
+ if (!job) return done({ content: `No background command with id "${id}".`, isError: true });
5133
+ const notes = [`[${id} ${describeState(job.state, job.exitCode, job.signal)}]`];
5134
+ if (job.truncated) notes.push("[output buffer full; earlier output was dropped]");
5135
+ const body = job.output.trim() === "" ? "(no new output)" : job.output.replace(/\n+$/, "");
5136
+ return done({
5137
+ content: [body, ...notes].join("\n"),
5138
+ isError: job.state === "exited" && job.exitCode !== null && job.exitCode !== 0
5139
+ });
5140
+ }
5141
+ };
5142
+ function describeState(state, exitCode, signal) {
5143
+ if (state === "running") return "still running";
5144
+ if (state === "killed") return `stopped${signal ? ` by ${signal}` : ""}`;
5145
+ return exitCode === 0 ? "finished" : `exit code ${exitCode}`;
5146
+ }
5147
+ function done(result) {
5148
+ return Promise.resolve(result);
5149
+ }
5150
+
4792
5151
  // src/tools/edit.ts
4793
5152
  import { readFile as readFile9, stat as stat6 } from "fs/promises";
4794
5153
 
4795
5154
  // src/tools/safe-write.ts
4796
5155
  import { randomUUID as randomUUID3 } from "crypto";
4797
5156
  import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm4 } from "fs/promises";
4798
- import { basename as basename2, dirname as dirname2, join } from "path";
5157
+ import { basename as basename2, dirname as dirname2, join as join2 } from "path";
4799
5158
  var UnsafeFileChangeError = class extends Error {
4800
5159
  constructor(message) {
4801
5160
  super(message);
@@ -4840,7 +5199,7 @@ async function readSafeFileSnapshot(file, maxBytes) {
4840
5199
  async function atomicWriteSafeFile(file, data, snapshot) {
4841
5200
  const parent = dirname2(file);
4842
5201
  await assertSameParent(parent, snapshot.parent);
4843
- const temp = join(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
5202
+ const temp = join2(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
4844
5203
  let handle;
4845
5204
  try {
4846
5205
  handle = await open(temp, "wx", snapshot.mode);
@@ -5030,58 +5389,10 @@ function replacement(before, oldString, newString, replaceAll) {
5030
5389
  return before.slice(0, at) + newString + before.slice(at + oldString.length);
5031
5390
  }
5032
5391
 
5033
- // src/tools/glob.ts
5034
- import { join as join2 } from "path";
5035
- import fg from "fast-glob";
5036
- var MAX_RESULTS = 500;
5037
- var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
5038
- var globTool = {
5039
- name: "glob",
5040
- description: 'Find files by glob pattern, most recently modified first. Supports patterns like "src/**/*.ts". Hidden files, node_modules, .git and dist are skipped.',
5041
- inputSchema: {
5042
- type: "object",
5043
- properties: {
5044
- pattern: { type: "string", description: "Glob pattern, e.g. **/*.test.ts" },
5045
- path: { type: "string", description: "Directory to search in, relative to the workspace root" }
5046
- },
5047
- required: ["pattern"],
5048
- additionalProperties: false
5049
- },
5050
- defaultPermission: "allow",
5051
- readOnly: true,
5052
- summarize(input) {
5053
- return `glob(${brief(input.pattern)})`;
5054
- },
5055
- async execute(input, ctx) {
5056
- const { pattern, path: path15 = "." } = input;
5057
- if (patternEscapes(pattern)) {
5058
- return { content: `Pattern ${pattern} must be relative to the workspace root.`, isError: true };
5059
- }
5060
- const safe = resolveInside(ctx.cwd, path15);
5061
- if (!safe.ok) return { content: safe.reason, isError: true };
5062
- const entries = await fg(pattern, {
5063
- cwd: safe.path,
5064
- dot: false,
5065
- onlyFiles: true,
5066
- followSymbolicLinks: false,
5067
- suppressErrors: true,
5068
- ignore: IGNORED_DIRECTORIES,
5069
- stats: true
5070
- });
5071
- if (entries.length === 0) return { content: `No files matched ${pattern}` };
5072
- entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
5073
- const paths = entries.slice(0, MAX_RESULTS).map((entry) => join2(safe.relative, entry.path));
5074
- if (entries.length > MAX_RESULTS) {
5075
- paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
5076
- }
5077
- return { content: paths.join("\n") };
5078
- }
5079
- };
5080
-
5081
5392
  // src/tools/grep.ts
5082
5393
  import { readFile as readFile10, stat as stat7 } from "fs/promises";
5083
5394
  import { join as join3 } from "path";
5084
- import fg2 from "fast-glob";
5395
+ import fg3 from "fast-glob";
5085
5396
 
5086
5397
  // src/tools/sensitive.ts
5087
5398
  import path10 from "path";
@@ -5259,7 +5570,7 @@ var grepTool = {
5259
5570
  if (!safe.ok) return { content: safe.reason, isError: true };
5260
5571
  const target = await stat7(safe.path).catch(() => null);
5261
5572
  if (!target) return { content: `Path not found: ${path15}`, isError: true };
5262
- const discovered = target.isDirectory() ? (await fg2(glob, {
5573
+ const discovered = target.isDirectory() ? (await fg3(glob, {
5263
5574
  cwd: safe.path,
5264
5575
  dot: false,
5265
5576
  onlyFiles: true,
@@ -5482,7 +5793,7 @@ var writeTool = {
5482
5793
 
5483
5794
  // src/tools/registry.ts
5484
5795
  function builtinTools() {
5485
- return [readTool, writeTool, editTool, bashTool, globTool, grepTool];
5796
+ return [readTool, writeTool, editTool, bashTool, bashOutputTool, globTool, grepTool];
5486
5797
  }
5487
5798
  function createToolRegistry(tools) {
5488
5799
  const byName = /* @__PURE__ */ new Map();
@@ -6040,16 +6351,15 @@ function unknownSkill(name, available) {
6040
6351
  // src/tools/task.ts
6041
6352
  import { randomBytes as randomBytes3 } from "crypto";
6042
6353
  var MAX_PROGRESS_LINES = 10;
6043
- var MAX_SUBAGENTS_PER_TURN = 3;
6044
6354
  var DESCRIPTION = [
6045
6355
  "Hand one self-contained sub-task to a subagent that works in its own fresh context and reports back a single summary.",
6046
6356
  "Use it when the task is genuinely independent and its intermediate output would be long or noisy: a wide search across the repository, reading many files to answer one question, tracing where something is used.",
6047
6357
  "Do not use it for work that is a couple of tool calls, and do not use it when you already have what you need \u2014 call the tools yourself instead.",
6048
6358
  'The subagent shares this filesystem and working directory but has NO memory of this conversation and cannot ask you anything, so "prompt" must stand alone: the goal, the constraints, and exactly what to report back.',
6359
+ "For multiple independent tasks, issue consecutive task calls in the same response so they can run concurrently. Do not delegate overlapping file edits.",
6049
6360
  "It runs to completion and you receive only its final message."
6050
6361
  ].join(" ");
6051
- function createTaskTool(runner, maxSubagents = MAX_SUBAGENTS_PER_TURN) {
6052
- const spawnsPerTurn = /* @__PURE__ */ new WeakMap();
6362
+ function createTaskTool(runner) {
6053
6363
  return {
6054
6364
  name: TASK_TOOL_NAME,
6055
6365
  description: DESCRIPTION,
@@ -6071,14 +6381,6 @@ function createTaskTool(runner, maxSubagents = MAX_SUBAGENTS_PER_TURN) {
6071
6381
  if (instruction === "") {
6072
6382
  return { content: "task needs a prompt: the full instruction for the subagent.", isError: true };
6073
6383
  }
6074
- const spawned = (spawnsPerTurn.get(ctx.signal) ?? 0) + 1;
6075
- spawnsPerTurn.set(ctx.signal, spawned);
6076
- if (spawned > maxSubagents) {
6077
- return {
6078
- content: `Already delegated ${maxSubagents} subagents while answering this message, which is the limit. Do the remaining work yourself with the tools you have.`,
6079
- isError: true
6080
- };
6081
- }
6082
6384
  const progress = [];
6083
6385
  const subagentId = `sa${randomBytes3(4).toString("hex")}`;
6084
6386
  const description = field(input, "description") || "subagent";
@@ -6217,8 +6519,8 @@ async function readJsonBounded(response) {
6217
6519
  let bytes = 0;
6218
6520
  try {
6219
6521
  while (true) {
6220
- const { done, value } = await reader.read();
6221
- if (done) break;
6522
+ const { done: done2, value } = await reader.read();
6523
+ if (done2) break;
6222
6524
  bytes += value.byteLength;
6223
6525
  if (bytes > MAX_RESPONSE_BYTES2) {
6224
6526
  await reader.cancel();
@@ -6391,9 +6693,9 @@ async function boot(options) {
6391
6693
  activeAgents.push(entry);
6392
6694
  const trackedRequest = {
6393
6695
  ...request,
6394
- onProgress: (summary) => {
6395
- entry.progress.push(summary);
6396
- request.onProgress(summary);
6696
+ onProgress: (summary2) => {
6697
+ entry.progress.push(summary2);
6698
+ request.onProgress(summary2);
6397
6699
  }
6398
6700
  };
6399
6701
  try {
@@ -6406,7 +6708,7 @@ async function boot(options) {
6406
6708
  }
6407
6709
  };
6408
6710
  tools.register([
6409
- createTaskTool(wrappedRunner, config.budget.maxSubagentsPerTurn)
6711
+ createTaskTool(wrappedRunner)
6410
6712
  ]);
6411
6713
  let persistQueue = Promise.resolve();
6412
6714
  let configQueue = Promise.resolve();
@@ -6461,13 +6763,15 @@ async function boot(options) {
6461
6763
  needsSetup: () => Object.keys(config.providers).length === 0,
6462
6764
  async addProvider(url, key) {
6463
6765
  const detected = await detectProvider(url, key);
6464
- rememberModels(detected.id, detected.models);
6766
+ for (const provider of detected) rememberModels(provider.id, provider.models);
6465
6767
  const configBefore = structuredClone(config);
6466
6768
  const authBefore = { ...auth };
6467
- config.providers[detected.id] = detected.config;
6468
- auth[detected.id] = key;
6469
- const chosen = preferredModel(detected.models);
6470
- if (chosen) config.model = formatModelRef(detected.id, chosen);
6769
+ for (const provider of detected) {
6770
+ config.providers[provider.id] = provider.config;
6771
+ auth[provider.id] = key;
6772
+ }
6773
+ const chosen = preferredDetectedModel(detected);
6774
+ if (chosen) config.model = formatModelRef(chosen.providerId, chosen.model.id);
6471
6775
  try {
6472
6776
  await saveConfig(config);
6473
6777
  await saveAuth(auth);
@@ -6480,11 +6784,12 @@ async function boot(options) {
6480
6784
  }
6481
6785
  registry = createRegistry(config, auth);
6482
6786
  const nextRef = config.model ?? "";
6483
- const discovered = detected.models.find((model) => model.id === chosen)?.contextWindow;
6484
- activateModel(nextRef, void 0, discovered);
6787
+ activateModel(nextRef, void 0, chosen?.model.contextWindow);
6485
6788
  void refreshModelContextWindow();
6486
- const kind = detected.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
6487
- return `${detected.id} \xB7 ${kind} \xB7 ${detected.models.length} models`;
6789
+ return detected.map((provider) => {
6790
+ const kind = provider.config.type === "anthropic" ? "Anthropic" : "OpenAI-compatible";
6791
+ return `${provider.id} \xB7 ${kind} \xB7 ${provider.models.length} models`;
6792
+ }).join("\n");
6488
6793
  },
6489
6794
  currentProviderId: () => parseModelRef(modelRef)?.provider,
6490
6795
  listProviderItems: () => Object.entries(config.providers).map(([id, entry]) => ({
@@ -6838,6 +7143,15 @@ ${lines.join("\n")}` : null;
6838
7143
  }
6839
7144
  return items;
6840
7145
  },
7146
+ async refreshProviderModels(providerId) {
7147
+ const models = await refreshModels(registry.get(providerId));
7148
+ rememberModels(providerId, models);
7149
+ if (parseModelRef(modelRef)?.provider === providerId) void refreshModelContextWindow();
7150
+ return models.length;
7151
+ },
7152
+ async listWorkspaceFiles(query) {
7153
+ return searchWorkspaceFiles(options.cwd, query);
7154
+ },
6841
7155
  async listPromptItems() {
6842
7156
  const prompts2 = await listPrompts();
6843
7157
  return prompts2.map((prompt) => ({
@@ -7033,6 +7347,7 @@ ${lines.join("\n")}` : null;
7033
7347
  try {
7034
7348
  await Promise.all([persistQueue, configQueue]);
7035
7349
  } finally {
7350
+ killAllJobs();
7036
7351
  await mcp.close();
7037
7352
  }
7038
7353
  }
@@ -7045,6 +7360,16 @@ function preferredModel(models) {
7045
7360
  }
7046
7361
  return models[0]?.id;
7047
7362
  }
7363
+ function preferredDetectedModel(detected) {
7364
+ const candidates = detected.flatMap(
7365
+ (provider) => provider.models.map((model) => ({ providerId: provider.id, model }))
7366
+ );
7367
+ for (const preferred of PREFERRED2) {
7368
+ const match = candidates.find((candidate) => candidate.model.id.endsWith(preferred));
7369
+ if (match) return match;
7370
+ }
7371
+ return candidates[0];
7372
+ }
7048
7373
 
7049
7374
  // src/app/ask.ts
7050
7375
  var RU_YES = /* @__PURE__ */ new Set(["y", "\u043D"]);
@@ -7313,6 +7638,7 @@ var COMMANDS = [
7313
7638
  { name: "provider" },
7314
7639
  { name: "logout", args: "[provider]" },
7315
7640
  { name: "key", args: "[provider]" },
7641
+ { name: "refresh", args: "[provider]" },
7316
7642
  { name: "effort" },
7317
7643
  { name: "thinking" },
7318
7644
  { name: "budget", args: "[tokens]" },
@@ -7362,22 +7688,22 @@ function matchCommands(line) {
7362
7688
  if (starts.length > 0) return starts;
7363
7689
  const contains = COMMANDS.filter((command) => command.name.includes(needle));
7364
7690
  if (contains.length > 0) return contains;
7365
- return COMMANDS.filter((command) => isSubsequence(needle, command.name));
7691
+ return COMMANDS.filter((command) => isSubsequence2(needle, command.name));
7366
7692
  }
7367
7693
  function closestCommand(name) {
7368
7694
  const needle = name.toLowerCase();
7369
7695
  let best;
7370
7696
  let bestScore = Number.POSITIVE_INFINITY;
7371
7697
  for (const command of COMMANDS) {
7372
- const score = editDistance(needle, command.name);
7373
- if (score < bestScore) {
7374
- bestScore = score;
7698
+ const score2 = editDistance(needle, command.name);
7699
+ if (score2 < bestScore) {
7700
+ bestScore = score2;
7375
7701
  best = command;
7376
7702
  }
7377
7703
  }
7378
7704
  return bestScore <= Math.max(2, Math.floor(needle.length / 2)) ? best : void 0;
7379
7705
  }
7380
- function isSubsequence(needle, haystack) {
7706
+ function isSubsequence2(needle, haystack) {
7381
7707
  let index = 0;
7382
7708
  for (const character of haystack) {
7383
7709
  if (character === needle[index]) index += 1;
@@ -7402,6 +7728,43 @@ function editDistance(a, b) {
7402
7728
  return previous[b.length] ?? 0;
7403
7729
  }
7404
7730
 
7731
+ // src/ui/mentions.ts
7732
+ var MENTION_PATTERN = /(?:^|\s)@([^\s@]+)/g;
7733
+ var TRAILING_PUNCTUATION = /[,;:!?]+$/;
7734
+ function mentionSpan(value, cursor) {
7735
+ const at = clamp(cursor, 0, value.length);
7736
+ const before = value.slice(0, at);
7737
+ const start = before.lastIndexOf("@");
7738
+ if (start === -1) return null;
7739
+ const preceding = start === 0 ? "" : value[start - 1] ?? "";
7740
+ if (preceding !== "" && !/\s/.test(preceding)) return null;
7741
+ const query = before.slice(start + 1);
7742
+ if (/[\s@]/.test(query)) return null;
7743
+ let end = at;
7744
+ while (end < value.length && !/[\s@]/.test(value[end] ?? "")) end += 1;
7745
+ return { start, end, query };
7746
+ }
7747
+ function completeMention(value, span, path15) {
7748
+ const rest = value.slice(span.end);
7749
+ const spaced = rest.startsWith(" ");
7750
+ const inserted = `@${path15}${spaced ? "" : " "}`;
7751
+ return {
7752
+ value: value.slice(0, span.start) + inserted + rest,
7753
+ cursor: span.start + inserted.length + (spaced ? 1 : 0)
7754
+ };
7755
+ }
7756
+ function extractMentions(text) {
7757
+ const found = [];
7758
+ for (const match of text.matchAll(MENTION_PATTERN)) {
7759
+ const path15 = (match[1] ?? "").replace(TRAILING_PUNCTUATION, "");
7760
+ if (path15 !== "" && !found.includes(path15)) found.push(path15);
7761
+ }
7762
+ return found;
7763
+ }
7764
+ function clamp(value, min, max) {
7765
+ return Math.max(min, Math.min(value, max));
7766
+ }
7767
+
7405
7768
  // src/ui/App.tsx
7406
7769
  import TextInput2 from "ink-text-input";
7407
7770
 
@@ -7503,6 +7866,7 @@ var en = {
7503
7866
  titleProvider: "Provider",
7504
7867
  titleLogout: "Sign out of a provider",
7505
7868
  titleKeyChange: "Change API key for a provider",
7869
+ titleModelRefresh: "Reload models for a provider",
7506
7870
  titleSessions: "Resume a session",
7507
7871
  titleSessionAction: "Session action",
7508
7872
  resumed: (id, count) => `Resumed ${id} \u2014 ${count} messages restored`,
@@ -7582,6 +7946,7 @@ ${url}`,
7582
7946
  budgetUnlimited: "Token budget: unlimited (0). Set /budget <tokens> to restore a limit.",
7583
7947
  budgetInvalid: "Invalid budget: must be between 1,000 and 10,000,000",
7584
7948
  modelSet: (value) => `Model: ${value}`,
7949
+ modelsRefreshed: (provider, count) => `${provider}: ${count} models reloaded from the endpoint.`,
7585
7950
  noModels: "No models available. Use /login to add a provider.",
7586
7951
  bypassOff: "Approval prompts are back on.",
7587
7952
  bypassAsk1: "Disable approval prompts for this session?",
@@ -7629,6 +7994,7 @@ ${url}`,
7629
7994
  provider: "switch between configured providers",
7630
7995
  logout: "choose a provider to sign out from",
7631
7996
  key: "change API key for a provider",
7997
+ refresh: "reload the model list for a provider",
7632
7998
  effort: "set reasoning depth",
7633
7999
  thinking: "toggle reasoning",
7634
8000
  budget: "set token budget per turn",
@@ -7692,6 +8058,7 @@ var ru = {
7692
8058
  titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
7693
8059
  titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
7694
8060
  titleKeyChange: "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
8061
+ titleModelRefresh: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u043C\u043E\u0434\u0435\u043B\u0438 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
7695
8062
  titleSessions: "\u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E",
7696
8063
  titleSessionAction: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0441\u0435\u0441\u0441\u0438\u0435\u0439",
7697
8064
  resumed: (id, count) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439: ${count}`,
@@ -7771,6 +8138,7 @@ ${url}`,
7771
8138
  budgetUnlimited: "\u041B\u0438\u043C\u0438\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432: \u0431\u0435\u0437\u043B\u0438\u043C\u0438\u0442\u043D\u044B\u0439 (0). \u0412\u0432\u0435\u0434\u0438 /budget <\u0447\u0438\u0441\u043B\u043E> \u0447\u0442\u043E\u0431\u044B \u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043B\u0438\u043C\u0438\u0442.",
7772
8139
  budgetInvalid: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442: \u043E\u0442 1 000 \u0434\u043E 10 000 000",
7773
8140
  modelSet: (value) => `\u041C\u043E\u0434\u0435\u043B\u044C: ${value}`,
8141
+ modelsRefreshed: (provider, count) => `${provider}: \u0441 \u044D\u043D\u0434\u043F\u043E\u0438\u043D\u0442\u0430 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 ${count}.`,
7774
8142
  noModels: "\u041C\u043E\u0434\u0435\u043B\u0435\u0439 \u043D\u0435\u0442. \u0414\u043E\u0431\u0430\u0432\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0447\u0435\u0440\u0435\u0437 /login.",
7775
8143
  bypassOff: "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u043D\u043E\u0432\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u044B.",
7776
8144
  bypassAsk1: "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u043D\u0430 \u044D\u0442\u0443 \u0441\u0435\u0441\u0441\u0438\u044E?",
@@ -7818,6 +8186,7 @@ ${url}`,
7818
8186
  provider: "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043C\u0435\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430\u043C\u0438",
7819
8187
  logout: "\u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0435\u0433\u043E \u043A\u043B\u044E\u0447",
7820
8188
  key: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
8189
+ refresh: "\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
7821
8190
  effort: "\u0433\u043B\u0443\u0431\u0438\u043D\u0430 \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u0439",
7822
8191
  thinking: "\u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u044F",
7823
8192
  budget: "\u043B\u0438\u043C\u0438\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0437\u0430 \u0445\u043E\u0434 (0 = \u0431\u0435\u0437\u043B\u0438\u043C\u0438\u0442)",
@@ -8306,7 +8675,8 @@ var PromptInput = memo(function PromptInput2({
8306
8675
  pending,
8307
8676
  hint,
8308
8677
  history,
8309
- attachments = []
8678
+ attachments = [],
8679
+ onListFiles
8310
8680
  }) {
8311
8681
  const theme = useTheme();
8312
8682
  const { columns } = useTerminalSize();
@@ -8321,9 +8691,28 @@ var PromptInput = memo(function PromptInput2({
8321
8691
  const cursorRef = useRef2(inputCursor);
8322
8692
  valueRef.current = safeValue;
8323
8693
  cursorRef.current = inputCursor;
8324
- const suggestions = matchCommands(safeValue);
8325
- const open3 = suggestions.length > 0;
8326
- const active2 = Math.min(selectionCursor, Math.max(0, suggestions.length - 1));
8694
+ const commandSuggestions = matchCommands(safeValue);
8695
+ const mention = onListFiles ? mentionSpan(safeValue, inputCursor) : null;
8696
+ const [files, setFiles] = useState4([]);
8697
+ const mentionQuery = mention?.query ?? null;
8698
+ useEffect(() => {
8699
+ if (mentionQuery === null || !onListFiles) {
8700
+ setFiles([]);
8701
+ return;
8702
+ }
8703
+ let live = true;
8704
+ void onListFiles(mentionQuery).then((found) => {
8705
+ if (live) setFiles(found);
8706
+ });
8707
+ return () => {
8708
+ live = false;
8709
+ };
8710
+ }, [mentionQuery, onListFiles]);
8711
+ const showFiles = mention !== null && files.length > 0;
8712
+ const suggestions = showFiles ? [] : commandSuggestions;
8713
+ const open3 = suggestions.length > 0 || showFiles;
8714
+ const optionCount = showFiles ? files.length : suggestions.length;
8715
+ const active2 = Math.min(selectionCursor, Math.max(0, optionCount - 1));
8327
8716
  useEffect(() => {
8328
8717
  setSelectionCursor(0);
8329
8718
  }, [safeValue]);
@@ -8331,14 +8720,14 @@ var PromptInput = memo(function PromptInput2({
8331
8720
  const length = characters2(safeValue).length;
8332
8721
  if (pendingValue.current === safeValue) {
8333
8722
  pendingValue.current = null;
8334
- setInputCursor((cursor) => clamp(cursor, 0, length));
8723
+ setInputCursor((cursor) => clamp2(cursor, 0, length));
8335
8724
  return;
8336
8725
  }
8337
8726
  setInputCursor(length);
8338
8727
  }, [safeValue]);
8339
8728
  const change = (next, cursor = characters2(next).length) => {
8340
8729
  next = sanitizeTerminalText(next);
8341
- const nextCursor2 = clamp(cursor, 0, characters2(next).length);
8730
+ const nextCursor2 = clamp2(cursor, 0, characters2(next).length);
8342
8731
  pendingValue.current = next;
8343
8732
  valueRef.current = next;
8344
8733
  cursorRef.current = nextCursor2;
@@ -8356,7 +8745,7 @@ var PromptInput = memo(function PromptInput2({
8356
8745
  const inserted = characters2(sanitizeTerminalText(pasted));
8357
8746
  if (inserted.length === 0) return;
8358
8747
  const valueCharacters = characters2(valueRef.current);
8359
- const cursor = clamp(cursorRef.current, 0, valueCharacters.length);
8748
+ const cursor = clamp2(cursorRef.current, 0, valueCharacters.length);
8360
8749
  valueCharacters.splice(cursor, 0, ...inserted);
8361
8750
  change(valueCharacters.join(""), cursor + inserted.length);
8362
8751
  };
@@ -8380,7 +8769,15 @@ var PromptInput = memo(function PromptInput2({
8380
8769
  return;
8381
8770
  }
8382
8771
  if (open3 && key.downArrow) {
8383
- setSelectionCursor(Math.min(suggestions.length - 1, active2 + 1));
8772
+ setSelectionCursor(Math.min(optionCount - 1, active2 + 1));
8773
+ return;
8774
+ }
8775
+ if (showFiles && (key.tab || key.return) && !key.shift) {
8776
+ const chosen = files[active2];
8777
+ if (chosen && mention) {
8778
+ const completed = completeMention(safeValue, mention, chosen);
8779
+ change(completed.value, completed.cursor);
8780
+ }
8384
8781
  return;
8385
8782
  }
8386
8783
  if (open3 && key.tab && !key.shift) {
@@ -8418,7 +8815,7 @@ var PromptInput = memo(function PromptInput2({
8418
8815
  return;
8419
8816
  }
8420
8817
  const valueCharacters = characters2(safeValue);
8421
- const cursor = clamp(inputCursor, 0, valueCharacters.length);
8818
+ const cursor = clamp2(inputCursor, 0, valueCharacters.length);
8422
8819
  if (key.leftArrow || key.rightArrow || key.home || key.end) {
8423
8820
  setInputCursor(nextCursor(cursor, valueCharacters.length, key));
8424
8821
  return;
@@ -8445,14 +8842,12 @@ var PromptInput = memo(function PromptInput2({
8445
8842
  });
8446
8843
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
8447
8844
  const visible = suggestions.slice(start, start + WINDOW2);
8845
+ const fileStart = Math.max(0, Math.min(active2 - WINDOW2 + 2, files.length - WINDOW2));
8448
8846
  return /* @__PURE__ */ jsxs8(Box8, { width: columns, maxWidth: "100%", flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8449
8847
  /* @__PURE__ */ jsxs8(
8450
8848
  Box8,
8451
8849
  {
8452
8850
  width: "100%",
8453
- borderStyle: "round",
8454
- borderColor: disabled ? theme.warn : theme.accent,
8455
- borderDimColor: disabled,
8456
8851
  paddingX: 1,
8457
8852
  children: [
8458
8853
  /* @__PURE__ */ jsx8(Text8, { color: disabled ? "gray" : theme.accent, children: "\u203A " }),
@@ -8473,6 +8868,18 @@ var PromptInput = memo(function PromptInput2({
8473
8868
  " ",
8474
8869
  sanitizeTerminalText(hint)
8475
8870
  ] }),
8871
+ showFiles && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
8872
+ files.slice(fileStart, fileStart + WINDOW2).map((file, index) => {
8873
+ const selected = fileStart + index === active2;
8874
+ return /* @__PURE__ */ jsxs8(Text8, { color: selected ? theme.accent : void 0, dimColor: !selected, children: [
8875
+ selected ? "\u276F " : " ",
8876
+ "@",
8877
+ sanitizeTerminalText(file)
8878
+ ] }, file);
8879
+ }),
8880
+ files.length > WINDOW2 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: strings.more(files.length - WINDOW2) }),
8881
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: strings.suggestHelp })
8882
+ ] }),
8476
8883
  open3 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
8477
8884
  visible.map((command, index) => {
8478
8885
  const selected = start + index === active2;
@@ -8498,7 +8905,7 @@ function EditableText({
8498
8905
  placeholder
8499
8906
  }) {
8500
8907
  const valueCharacters = characters2(value);
8501
- const at = clamp(cursor, 0, valueCharacters.length);
8908
+ const at = clamp2(cursor, 0, valueCharacters.length);
8502
8909
  if (valueCharacters.length === 0) {
8503
8910
  const placeholderCharacters = characters2(sanitizeTerminalText(placeholder));
8504
8911
  return /* @__PURE__ */ jsxs8(Text8, { children: [
@@ -8522,7 +8929,7 @@ function nextCursor(cursor, length, key) {
8522
8929
  function characters2(value) {
8523
8930
  return [...value];
8524
8931
  }
8525
- function clamp(value, min, max) {
8932
+ function clamp2(value, min, max) {
8526
8933
  return Math.max(min, Math.min(value, max));
8527
8934
  }
8528
8935
 
@@ -8771,13 +9178,18 @@ import Spinner2 from "ink-spinner";
8771
9178
  import stringWidth2 from "string-width";
8772
9179
 
8773
9180
  // src/ui/markdown.tsx
8774
- import { Box as Box11, Text as Text10 } from "ink";
8775
- import { Fragment as Fragment4, useMemo as useMemo3 } from "react";
9181
+ import { Box as Box11, Text as InkText } from "ink";
9182
+ import { createContext as createContext3, Fragment as Fragment4, useContext as useContext3, useMemo as useMemo3 } from "react";
8776
9183
  import stringWidth from "string-width";
8777
9184
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8778
- function Markdown({ children }) {
9185
+ var DimContext = createContext3(false);
9186
+ function Text10({ dimColor, color, ...props }) {
9187
+ const dim = useContext3(DimContext);
9188
+ return /* @__PURE__ */ jsx11(InkText, { ...props, color: dim ? void 0 : color, dimColor: dimColor ?? dim });
9189
+ }
9190
+ function Markdown({ children, dim = false }) {
8779
9191
  const blocks = useMemo3(() => extractBlocks(children), [children]);
8780
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
9192
+ return /* @__PURE__ */ jsx11(DimContext.Provider, { value: dim, children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) }) });
8781
9193
  }
8782
9194
  function BlockView({ block }) {
8783
9195
  switch (block.type) {
@@ -9425,7 +9837,7 @@ function AssistantView({ bubble: source, maxRows }) {
9425
9837
  const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9426
9838
  const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9427
9839
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9428
- frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { children: frameThinking }) }),
9840
+ frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { dim: true, children: frameThinking }) }),
9429
9841
  bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9430
9842
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9431
9843
  /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
@@ -9929,6 +10341,11 @@ function App({
9929
10341
  })
9930
10342
  };
9931
10343
  try {
10344
+ try {
10345
+ await runtime.persist(history.current);
10346
+ } catch (error) {
10347
+ notice("error", error instanceof Error ? error.message : String(error));
10348
+ }
9932
10349
  history.current = await runtime.run(history.current, hooks, controller.signal);
9933
10350
  void runtime.persist(history.current).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
9934
10351
  } catch (error) {
@@ -9957,7 +10374,6 @@ function App({
9957
10374
  const command = (firstSpace === -1 ? body : body.slice(0, firstSpace)).toLowerCase();
9958
10375
  const rawRest = firstSpace === -1 ? "" : body.slice(firstSpace).trim();
9959
10376
  const rest = rawRest ? rawRest.split(/\s+/) : [];
9960
- setInput("");
9961
10377
  switch (command) {
9962
10378
  case "help":
9963
10379
  notice(
@@ -10256,6 +10672,29 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
10256
10672
  }
10257
10673
  return;
10258
10674
  }
10675
+ case "refresh": {
10676
+ const providers = runtime.listProviderItems();
10677
+ if (providers.length === 0) {
10678
+ notice("warn", strings.logoutNothing);
10679
+ return;
10680
+ }
10681
+ let providerId = rest[0];
10682
+ if (!providerId) {
10683
+ providerId = providers.length === 1 ? providers[0]?.key : await pick(strings.titleModelRefresh, providers) ?? void 0;
10684
+ }
10685
+ if (!providerId) return;
10686
+ if (!providers.some((provider) => provider.key === providerId)) {
10687
+ notice("warn", `Provider "${providerId}" is not configured.`);
10688
+ return;
10689
+ }
10690
+ try {
10691
+ const count = await runtime.refreshProviderModels(providerId);
10692
+ notice("info", strings.modelsRefreshed(providerId, count));
10693
+ } catch (error) {
10694
+ notice("error", error instanceof Error ? error.message : String(error));
10695
+ }
10696
+ return;
10697
+ }
10259
10698
  case "lang": {
10260
10699
  const choice = await pick(
10261
10700
  strings.titleLang,
@@ -10850,6 +11289,10 @@ Rename the file if you want a different name.`);
10850
11289
  },
10851
11290
  [appendAttachment, notice, runtime, strings]
10852
11291
  );
11292
+ const listWorkspaceFiles = useCallback(
11293
+ (query) => runtime.listWorkspaceFiles(query).catch(() => []),
11294
+ [runtime]
11295
+ );
10853
11296
  const pasteClipboardImage = useCallback(() => {
10854
11297
  if (clipboardPasteTask.current) return;
10855
11298
  if (attachmentsRef.current.length >= MAX_ATTACHMENTS) {
@@ -10888,7 +11331,9 @@ Rename the file if you want a different name.`);
10888
11331
  const text = raw.trim();
10889
11332
  const submitSlash = () => {
10890
11333
  if (!detachedInput) setInput("");
10891
- if (busyRef.current || slashPendingRef.current > 0) {
11334
+ const command = text.slice(1).trim().split(/\s+/)[0]?.toLowerCase();
11335
+ const liveCommand = ["help", "usage", "subagents", "config", "exit", "quit"].includes(command ?? "");
11336
+ if (busyRef.current && !liveCommand || slashPendingRef.current > 0) {
10892
11337
  queueRef.current = [...queueRef.current, { kind: "command", line: text }];
10893
11338
  setPendingCount(queueRef.current.length);
10894
11339
  return;
@@ -10913,11 +11358,21 @@ Rename the file if you want a different name.`);
10913
11358
  }
10914
11359
  const queuedAttachments = attachmentsRef.current;
10915
11360
  if (!text && queuedAttachments.length === 0) return;
11361
+ const mentioned = [];
11362
+ for (const path15 of extractMentions(text)) {
11363
+ if (queuedAttachments.length + mentioned.length >= MAX_ATTACHMENTS) break;
11364
+ try {
11365
+ mentioned.push(await runtime.loadAttachment(path15));
11366
+ } catch {
11367
+ }
11368
+ }
11369
+ if (generation !== attachmentGeneration.current) return;
10916
11370
  if (text) setPromptHistory((state) => appendInputHistory(state, text));
10917
11371
  if (!detachedInput) setInput("");
10918
11372
  const content = [
10919
11373
  ...text ? [{ type: "text", text }] : [],
10920
- ...queuedAttachments
11374
+ ...queuedAttachments,
11375
+ ...mentioned
10921
11376
  ];
10922
11377
  replaceAttachments([]);
10923
11378
  const item = { kind: "message", text, content };
@@ -10992,11 +11447,11 @@ Rename the file if you want a different name.`);
10992
11447
  Onboarding,
10993
11448
  {
10994
11449
  onSubmit: async (url, key) => {
10995
- const summary = await runtime.addProvider(url, key);
11450
+ const summary2 = await runtime.addProvider(url, key);
10996
11451
  setSetup(false);
10997
- notice("info", `${summary}
11452
+ notice("info", `${summary2}
10998
11453
  ${strings.configAt(runtime.configPath())}`);
10999
- return summary;
11454
+ return summary2;
11000
11455
  }
11001
11456
  }
11002
11457
  )
@@ -11073,6 +11528,7 @@ ${strings.configAt(runtime.configPath())}`);
11073
11528
  onSubmit: submit,
11074
11529
  onPastePath: tryQueueAutomaticAttachment,
11075
11530
  onPasteImage: pasteClipboardImage,
11531
+ onListFiles: listWorkspaceFiles,
11076
11532
  disabled: busy,
11077
11533
  pending: pendingCount,
11078
11534
  hint: busy ? strings.escCancel : void 0,