@kernelonpanic/kitcode 1.2.0 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -69,9 +69,6 @@ var diagnosticsSchema = z.object({
69
69
  autoRun: z.boolean().default(true),
70
70
  commands: z.array(z.string().trim().min(1).max(2e4)).max(8).default([])
71
71
  });
72
- var updatesSchema = z.object({
73
- checkOnStart: z.boolean().default(true)
74
- });
75
72
  var configSchema = z.object({
76
73
  version: z.literal(1).default(1),
77
74
  model: z.string().optional(),
@@ -86,7 +83,6 @@ var configSchema = z.object({
86
83
  maxSubagentsPerTurn: 3
87
84
  }),
88
85
  diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
89
- updates: updatesSchema.default({ checkOnStart: false }),
90
86
  providers: z.record(providerIdSchema, providerConfigSchema).default({}),
91
87
  permissions: z.record(safeRecordKeySchema, permissionModeSchema).default({}),
92
88
  mcp: z.record(safeRecordKeySchema, mcpServerSchema).default({})
@@ -153,19 +149,19 @@ var CONNECTION_REASONS = {
153
149
  DEPTH_ZERO_SELF_SIGNED_CERT: "the TLS certificate is self-signed",
154
150
  UNABLE_TO_VERIFY_LEAF_SIGNATURE: "the TLS certificate chain cannot be verified"
155
151
  };
156
- function describeProviderFailure(error, providerId) {
152
+ function describeProviderFailure(error, providerId, knownSecrets = []) {
157
153
  const abort = abortKind(error);
158
154
  if (abort === "user") return `Request to "${providerId}" was cancelled.`;
159
155
  if (abort === "timeout") {
160
156
  return `Request to "${providerId}" ran out of time before the provider answered \u2014 it may be overloaded, try again.`;
161
157
  }
162
158
  const status = statusOf(error);
163
- if (status !== void 0) return describeStatus(status, providerId, error);
164
- const reason = connectionReason(error);
159
+ if (status !== void 0) return describeStatus(status, providerId, error, knownSecrets);
160
+ const reason = connectionReason(error, knownSecrets);
165
161
  if (reason) {
166
162
  return `Cannot reach "${providerId}": ${reason}. Check the base URL and your network.`;
167
163
  }
168
- return `Request to "${providerId}" failed \u2014 ${snippet(messageOf(error))}`;
164
+ return `Request to "${providerId}" failed \u2014 ${snippet(messageOf(error), 160, knownSecrets)}`;
169
165
  }
170
166
  function statusOf(error) {
171
167
  const value = prop(error, "status") ?? prop(error, "statusCode");
@@ -177,8 +173,8 @@ function isUserAbort(error, signal) {
177
173
  function isConnectionFailure(error) {
178
174
  return connectionReason(error) !== void 0;
179
175
  }
180
- function toProviderError(error, providerId) {
181
- return new ProviderError(describeProviderFailure(error, providerId), providerId, statusOf(error), {
176
+ function toProviderError(error, providerId, knownSecrets = []) {
177
+ return new ProviderError(describeProviderFailure(error, providerId, knownSecrets), providerId, statusOf(error), {
182
178
  cause: error
183
179
  });
184
180
  }
@@ -226,8 +222,8 @@ async function readHead(stream) {
226
222
  }
227
223
  return text;
228
224
  }
229
- async function invalidStreamError(providerId, capture, cause) {
230
- const excerpt = snippet(await capture.head(), EXCERPT_CHARS);
225
+ async function invalidStreamError(providerId, capture, cause, knownSecrets = []) {
226
+ const excerpt = snippet(await capture.head(), EXCERPT_CHARS, knownSecrets);
231
227
  const detail = excerpt === "" ? "the body was empty" : `the body began: ${excerpt}`;
232
228
  return new ProviderError(
233
229
  `"${providerId}" returned a response that is not a valid streaming chat completion \u2014 no stream events arrived and ${detail}. The endpoint may speak a different protocol than the one configured for it.`,
@@ -236,8 +232,8 @@ async function invalidStreamError(providerId, capture, cause) {
236
232
  cause === void 0 ? void 0 : { cause }
237
233
  );
238
234
  }
239
- function describeStatus(status, providerId, error) {
240
- const detail = detailOf(error, status);
235
+ function describeStatus(status, providerId, error, knownSecrets) {
236
+ const detail = detailOf(error, status, knownSecrets);
241
237
  const wait = retryAfterSeconds(error);
242
238
  const after = wait === void 0 ? void 0 : `retry after ${wait}s`;
243
239
  if (status === 401) {
@@ -272,7 +268,7 @@ function abortKind(error) {
272
268
  if (name === "AbortError") return "user";
273
269
  return void 0;
274
270
  }
275
- function connectionReason(error) {
271
+ function connectionReason(error, knownSecrets = []) {
276
272
  const links = chain(error);
277
273
  for (const link of links) {
278
274
  for (const candidate of [link, ...siblings(link)]) {
@@ -282,9 +278,11 @@ function connectionReason(error) {
282
278
  }
283
279
  }
284
280
  const deepest = links[links.length - 1];
285
- if (links.length > 1 && deepest instanceof Error) return snippet(deepest.message);
281
+ if (links.length > 1 && deepest instanceof Error) {
282
+ return snippet(deepest.message, 160, knownSecrets);
283
+ }
286
284
  const message = messageOf(error);
287
- return /fetch failed|connection error|socket|network/i.test(message) ? snippet(message) : void 0;
285
+ return /fetch failed|connection error|socket|network/i.test(message) ? snippet(message, 160, knownSecrets) : void 0;
288
286
  }
289
287
  function chain(error) {
290
288
  const links = [];
@@ -317,9 +315,9 @@ function header(error, name) {
317
315
  const value = headers[name];
318
316
  return typeof value === "string" ? value : void 0;
319
317
  }
320
- function detailOf(error, status) {
318
+ function detailOf(error, status, knownSecrets) {
321
319
  const raw = bodyMessage(error) ?? messageOf(error).replace(new RegExp(`^${status}\\s+`), "");
322
- const text = snippet(raw);
320
+ const text = snippet(raw, 160, knownSecrets);
323
321
  if (text === "" || text === String(status) || text === "status code (no body)") return "";
324
322
  return ` \u2014 ${text}`;
325
323
  }
@@ -336,8 +334,8 @@ function messageOf(error) {
336
334
  if (error === null || error === void 0) return "unknown error";
337
335
  return String(error);
338
336
  }
339
- function snippet(text, limit = 160) {
340
- return redactSecrets(oneLine(text)).slice(0, limit);
337
+ function snippet(text, limit = 160, knownSecrets = []) {
338
+ return redactSecrets(oneLine(text), knownSecrets).slice(0, limit);
341
339
  }
342
340
  function oneLine(text) {
343
341
  return text.replace(/\s+/g, " ").trim();
@@ -454,7 +452,7 @@ async function detectProvider(rawUrl, apiKey2, opts = {}) {
454
452
  }
455
453
  if (!attempt.ok && attempt.status !== 404 && attempt.status !== 0) break;
456
454
  }
457
- throw new Error(describeFailure(last));
455
+ throw new Error(describeFailure(last, apiKey2));
458
456
  }
459
457
  function normaliseBaseUrl(rawUrl) {
460
458
  return rawUrl.trim().replace(/\/+$/, "");
@@ -591,9 +589,9 @@ function stringList(value) {
591
589
  if (!Array.isArray(value)) return [];
592
590
  return value.filter((item) => typeof item === "string");
593
591
  }
594
- function describeFailure(attempt) {
592
+ function describeFailure(attempt, apiKey2) {
595
593
  const status = attempt.status === 0 ? "request failed" : `HTTP ${attempt.status}`;
596
- const snippet2 = redactSecrets(attempt.text.trim()).slice(0, 200);
594
+ const snippet2 = redactSecrets(attempt.text.trim(), [apiKey2]).slice(0, 200);
597
595
  const detail = snippet2 === "" ? "" : `: ${snippet2}`;
598
596
  return `No OpenAI- or Anthropic-compatible API found at ${attempt.url} (${status})${detail}`;
599
597
  }
@@ -757,9 +755,16 @@ async function configLocation() {
757
755
  active ??= await resolveConfigLocation();
758
756
  return active;
759
757
  }
758
+ async function loadConfig(cwd) {
759
+ const location = await resolveConfigLocation(cwd);
760
+ return loadConfigAt(location);
761
+ }
760
762
  async function loadProjectConfig(dir) {
761
763
  return loadConfigAt({ path: projectConfigPath(dir), scope: "project" });
762
764
  }
765
+ async function loadGlobalConfig() {
766
+ return loadConfigAt({ path: configPath, scope: "global" });
767
+ }
763
768
  async function loadRuntimeConfig(cwd) {
764
769
  const location = await resolveConfigLocation(cwd);
765
770
  if (location.scope === "project" && !await isWorkspaceTrusted(cwd)) {
@@ -886,7 +891,7 @@ async function addProvider(url, key, options = {}) {
886
891
  await initProjectConfig(cwd);
887
892
  await trustWorkspace(cwd);
888
893
  }
889
- const config = options.local ? await loadProjectConfig(cwd) : (await loadRuntimeConfig(cwd)).config;
894
+ const config = options.local ? await loadProjectConfig(cwd) : process.env.KITCODE_CONFIG ? await loadConfig(cwd) : await loadGlobalConfig();
890
895
  const auth = await loadAuth();
891
896
  const configBefore = structuredClone(config);
892
897
  const authBefore = { ...auth };
@@ -1299,12 +1304,12 @@ function exportFileName(state) {
1299
1304
  }
1300
1305
  function renderSessionMarkdown(state) {
1301
1306
  const lines = [
1302
- `# ${state.title || `KitCode session ${shortSessionId(state.id)}`}`,
1307
+ `# ${escapeMarkdownInline(state.title || `KitCode session ${shortSessionId(state.id)}`)}`,
1303
1308
  "",
1304
- `- Session: ${state.id}`,
1305
- `- Workspace: ${state.cwd}`,
1306
- `- Model: ${state.model || "unknown"}`,
1307
- `- Updated: ${state.updatedAt}`,
1309
+ `- Session: ${escapeMarkdownInline(state.id)}`,
1310
+ `- Workspace: ${escapeMarkdownInline(state.cwd)}`,
1311
+ `- Model: ${escapeMarkdownInline(state.model || "unknown")}`,
1312
+ `- Updated: ${escapeMarkdownInline(state.updatedAt)}`,
1308
1313
  ""
1309
1314
  ];
1310
1315
  for (const message of state.messages) {
@@ -1319,18 +1324,41 @@ function renderMessage(content) {
1319
1324
  const sections = [];
1320
1325
  for (const block of content) {
1321
1326
  if (block.type === "text") sections.push(block.text);
1322
- else if (block.type === "image") sections.push(`[Image attached: ${block.name}]`);
1323
- else if (block.type === "file") sections.push(`[File attached: ${block.name}]
1327
+ else if (block.type === "image") {
1328
+ sections.push(`[Image attached: ${escapeMarkdownInline(block.name)}]`);
1329
+ } else if (block.type === "file") {
1330
+ sections.push(
1331
+ `[File attached: ${escapeMarkdownInline(block.name)}]
1324
1332
 
1325
- ${block.text}`);
1326
- else if (block.type === "tool_use") sections.push(`> Tool: ${block.name}`);
1327
- else if (block.type === "tool_result") {
1333
+ ${markdownCodeFence(block.text)}`
1334
+ );
1335
+ } else if (block.type === "tool_use") {
1336
+ sections.push(`> Tool: ${escapeMarkdownInline(block.name)}`);
1337
+ } else if (block.type === "tool_result") {
1338
+ const result = block.content.replace(/\r\n?/g, "\n").split("\n").map((line) => `> ${line}`).join("\n");
1328
1339
  sections.push(`> Tool result${block.isError ? " (error)" : ""}:
1329
- > ${block.content.replace(/\n/g, "\n> ")}`);
1340
+ ${result}`);
1330
1341
  }
1331
1342
  }
1332
1343
  return sections.join("\n\n").trim();
1333
1344
  }
1345
+ function escapeMarkdownInline(value) {
1346
+ return value.replace(/[\r\n\t]+/g, " ").replace(/([\\`*_[\]<>])/g, "\\$1");
1347
+ }
1348
+ function markdownCodeFence(value) {
1349
+ const normalized = value.replace(/\r\n?/g, "\n");
1350
+ const backtickLength = longestMarkerRun(normalized, /`+/g) + 1;
1351
+ const tildeLength = longestMarkerRun(normalized, /~+/g) + 1;
1352
+ const marker = backtickLength <= tildeLength ? "`" : "~";
1353
+ const fence = marker.repeat(Math.max(3, Math.min(backtickLength, tildeLength)));
1354
+ return `${fence}
1355
+ ${normalized}${normalized.endsWith("\n") ? "" : "\n"}${fence}`;
1356
+ }
1357
+ function longestMarkerRun(value, pattern) {
1358
+ let longest = 0;
1359
+ for (const match of value.matchAll(pattern)) longest = Math.max(longest, match[0].length);
1360
+ return longest;
1361
+ }
1334
1362
  function readContextUsage(value) {
1335
1363
  if (typeof value !== "object" || value === null) return void 0;
1336
1364
  const candidate = value;
@@ -2509,8 +2537,8 @@ function looksLikeAttachmentPath(value) {
2509
2537
  if (path7.isAbsolute(candidate)) return true;
2510
2538
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
2511
2539
  if (candidate.includes("/") || candidate.includes("\\")) return true;
2512
- const basename2 = path7.basename(candidate).toLowerCase();
2513
- return path7.extname(basename2) !== "" || AUTO_PATH_NAMES.has(basename2);
2540
+ const basename3 = path7.basename(candidate).toLowerCase();
2541
+ return path7.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
2514
2542
  }
2515
2543
  async function loadClipboardImage(platform = process.platform, runner = runClipboardCommand) {
2516
2544
  const commands = clipboardCommands(platform);
@@ -2586,8 +2614,8 @@ function resolveAttachmentPath(cwd, requestedPath) {
2586
2614
  return path7.isAbsolute(expanded) ? path7.normalize(expanded) : path7.resolve(cwd, expanded);
2587
2615
  }
2588
2616
  function isSensitiveAutomaticPath(file) {
2589
- const basename2 = path7.basename(file).toLowerCase();
2590
- return basename2.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename2) || SENSITIVE_AUTO_EXTENSIONS.has(path7.extname(basename2));
2617
+ const basename3 = path7.basename(file).toLowerCase();
2618
+ return basename3.startsWith(".env.") || SENSITIVE_AUTO_NAMES.has(basename3) || SENSITIVE_AUTO_EXTENSIONS.has(path7.extname(basename3));
2591
2619
  }
2592
2620
  function attachmentLabel(block) {
2593
2621
  if (block.type === "image") return `image: ${block.name}`;
@@ -3424,7 +3452,7 @@ function clip(value) {
3424
3452
  // package.json
3425
3453
  var package_default = {
3426
3454
  name: "@kernelonpanic/kitcode",
3427
- version: "1.2.0",
3455
+ version: "1.2.5",
3428
3456
  description: "Terminal coding agent with a config you never have to write by hand",
3429
3457
  type: "module",
3430
3458
  license: "MIT",
@@ -3465,6 +3493,7 @@ var package_default = {
3465
3493
  "ink-text-input": "^6.0.0",
3466
3494
  openai: "^7.3.0",
3467
3495
  react: "^19.2.8",
3496
+ "string-width": "^8.2.2",
3468
3497
  zod: "^4.4.3"
3469
3498
  },
3470
3499
  devDependencies: {
@@ -3491,8 +3520,7 @@ var package_default = {
3491
3520
 
3492
3521
  // src/version.ts
3493
3522
  var KITCODE_VERSION = package_default.version;
3494
- var KITCODE_REPOSITORY = "KernelEditor/KitCode";
3495
- var KITCODE_COMMIT = true ? "c93b03ff85112af7b4c28f543aff6b9377f84ad6" : "development";
3523
+ var KITCODE_COMMIT = true ? "e9d5bdd60b06f2ee8d2d28f0d53b13d68298ae6b" : "development";
3496
3524
 
3497
3525
  // src/mcp/client.ts
3498
3526
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3633,9 +3661,9 @@ function createMcpManager(servers) {
3633
3661
  serverStates.delete(name);
3634
3662
  },
3635
3663
  async close() {
3636
- const open2 = [...sessions.values()];
3664
+ const open3 = [...sessions.values()];
3637
3665
  sessions.clear();
3638
- await Promise.all(open2.map((session) => session.client.close().catch(() => {
3666
+ await Promise.all(open3.map((session) => session.client.close().catch(() => {
3639
3667
  })));
3640
3668
  },
3641
3669
  states() {
@@ -4071,12 +4099,12 @@ function createAnthropicProvider(args) {
4071
4099
  return {
4072
4100
  id: args.id,
4073
4101
  kind: "anthropic",
4074
- stream: (req) => streamTurn(client, args.id, req),
4075
- listModels: () => listModels(client, args.id),
4102
+ stream: (req) => streamTurn(client, args.id, args.apiKey, req),
4103
+ listModels: () => listModels(client, args.id, args.apiKey),
4076
4104
  knownModels: () => KNOWN_MODELS
4077
4105
  };
4078
4106
  }
4079
- async function* streamTurn(client, providerId, req) {
4107
+ async function* streamTurn(client, providerId, apiKey2, req) {
4080
4108
  const params = {
4081
4109
  model: req.model,
4082
4110
  max_tokens: req.maxTokens,
@@ -4117,9 +4145,9 @@ async function* streamTurn(client, providerId, req) {
4117
4145
  const limits2 = parseRateLimits(capture.headers());
4118
4146
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4119
4147
  if (events === 0 && capture.succeeded() && !isConnectionFailure(error)) {
4120
- throw await invalidStreamError(providerId, capture, error);
4148
+ throw await invalidStreamError(providerId, capture, error, [apiKey2]);
4121
4149
  }
4122
- throw toProviderError(error, providerId);
4150
+ throw toProviderError(error, providerId, [apiKey2]);
4123
4151
  }
4124
4152
  const stopReason = STOP_REASONS[final.stop_reason ?? ""] ?? "end_turn";
4125
4153
  const content = toContentBlocks(final.content);
@@ -4138,7 +4166,7 @@ async function* streamTurn(client, providerId, req) {
4138
4166
  ...stopReason === "refusal" ? { refusal: toRefusal(final.stop_details) } : {}
4139
4167
  };
4140
4168
  }
4141
- async function listModels(client, providerId) {
4169
+ async function listModels(client, providerId, apiKey2) {
4142
4170
  try {
4143
4171
  const models = [];
4144
4172
  for await (const model of client.models.list({ limit: 100 })) {
@@ -4152,7 +4180,7 @@ async function listModels(client, providerId) {
4152
4180
  }
4153
4181
  return models;
4154
4182
  } catch (error) {
4155
- throw toProviderError(error, providerId);
4183
+ throw toProviderError(error, providerId, [apiKey2]);
4156
4184
  }
4157
4185
  }
4158
4186
  function toMessageParams(messages) {
@@ -4256,12 +4284,12 @@ function createOpenAiProvider(args) {
4256
4284
  return {
4257
4285
  id: args.id,
4258
4286
  kind: "openai",
4259
- stream: (req) => streamTurn2(client, args.id, req),
4260
- listModels: () => listModels2(client, args.id),
4287
+ stream: (req) => streamTurn2(client, args.id, args.apiKey, req),
4288
+ listModels: () => listModels2(client, args.id, args.apiKey),
4261
4289
  knownModels: () => []
4262
4290
  };
4263
4291
  }
4264
- async function* streamTurn2(client, providerId, req) {
4292
+ async function* streamTurn2(client, providerId, apiKey2, req) {
4265
4293
  let text = "";
4266
4294
  let thinking = "";
4267
4295
  let finishReason = null;
@@ -4309,19 +4337,20 @@ async function* streamTurn2(client, providerId, req) {
4309
4337
  if (!isUserAbort(error, req.signal)) {
4310
4338
  const limits2 = parseRateLimits(capture.headers());
4311
4339
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4312
- throw toProviderError(error, providerId);
4340
+ throw toProviderError(error, providerId, [apiKey2]);
4313
4341
  }
4314
4342
  aborted = true;
4315
4343
  }
4316
- const content = toContentBlocks2(thinking, text, calls);
4317
4344
  if (aborted || req.signal?.aborted) {
4345
+ const content2 = toTextContentBlocks(thinking, text);
4318
4346
  if (sawUsage) yield { type: "usage", usage };
4319
4347
  const limits2 = parseRateLimits(capture.headers());
4320
4348
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4321
- yield { type: "done", stopReason: "aborted", content };
4349
+ yield { type: "done", stopReason: "aborted", content: content2 };
4322
4350
  return;
4323
4351
  }
4324
- if (recognised === 0) throw await invalidStreamError(providerId, capture);
4352
+ if (recognised === 0) throw await invalidStreamError(providerId, capture, void 0, [apiKey2]);
4353
+ const content = toContentBlocks2(providerId, thinking, text, calls);
4325
4354
  for (const block of content) {
4326
4355
  if (block.type === "tool_use") {
4327
4356
  yield { type: "tool_call", id: block.id, name: block.name, input: block.input };
@@ -4338,7 +4367,7 @@ function resolveStop(finishReason, hasCalls) {
4338
4367
  if (hasCalls) return "tool_use";
4339
4368
  return mapped ?? "end_turn";
4340
4369
  }
4341
- async function listModels2(client, providerId) {
4370
+ async function listModels2(client, providerId, apiKey2) {
4342
4371
  try {
4343
4372
  const models = [];
4344
4373
  for await (const model of client.models.list()) {
@@ -4347,7 +4376,7 @@ async function listModels2(client, providerId) {
4347
4376
  }
4348
4377
  return models;
4349
4378
  } catch (error) {
4350
- throw toProviderError(error, providerId);
4379
+ throw toProviderError(error, providerId, [apiKey2]);
4351
4380
  }
4352
4381
  }
4353
4382
  function accumulate(calls, delta) {
@@ -4357,10 +4386,8 @@ function accumulate(calls, delta) {
4357
4386
  if (delta.function?.arguments) call.args += delta.function.arguments;
4358
4387
  calls.set(delta.index, call);
4359
4388
  }
4360
- function toContentBlocks2(thinking, text, calls) {
4361
- const content = [];
4362
- if (thinking) content.push({ type: "thinking", text: thinking });
4363
- if (text) content.push({ type: "text", text });
4389
+ function toContentBlocks2(providerId, thinking, text, calls) {
4390
+ const content = toTextContentBlocks(thinking, text);
4364
4391
  const sorted = [...calls.entries()].sort((a, b) => a[0] - b[0]);
4365
4392
  const tmpId = (position) => `call_${position}`;
4366
4393
  for (const [position, [, call]] of sorted.entries()) {
@@ -4368,18 +4395,34 @@ function toContentBlocks2(thinking, text, calls) {
4368
4395
  type: "tool_use",
4369
4396
  id: call.id || tmpId(position),
4370
4397
  name: call.name,
4371
- input: parseArguments(call.args)
4398
+ input: parseArguments(providerId, call.name, call.args)
4372
4399
  });
4373
4400
  }
4374
4401
  return content;
4375
4402
  }
4376
- function parseArguments(args) {
4377
- if (!args) return {};
4403
+ function toTextContentBlocks(thinking, text) {
4404
+ const content = [];
4405
+ if (thinking) content.push({ type: "thinking", text: thinking });
4406
+ if (text) content.push({ type: "text", text });
4407
+ return content;
4408
+ }
4409
+ function parseArguments(providerId, toolName, args) {
4410
+ let parsed;
4378
4411
  try {
4379
- return JSON.parse(args);
4412
+ parsed = JSON.parse(args);
4380
4413
  } catch {
4381
- return {};
4414
+ throw new ProviderError(
4415
+ `Tool "${toolName || "unknown"}" returned malformed JSON arguments; the tool was not run.`,
4416
+ providerId
4417
+ );
4418
+ }
4419
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4420
+ throw new ProviderError(
4421
+ `Tool "${toolName || "unknown"}" arguments must be a JSON object; the tool was not run.`,
4422
+ providerId
4423
+ );
4382
4424
  }
4425
+ return parsed;
4383
4426
  }
4384
4427
  function toChatMessages(system, messages) {
4385
4428
  const out = [{ role: "system", content: system }];
@@ -4512,7 +4555,7 @@ function createPermissionEngine(configPermissions) {
4512
4555
  decide(tool, requested) {
4513
4556
  const configured = resolve3(tool, requested);
4514
4557
  if (configured === "deny") return "deny";
4515
- if (current === "plan" && configured !== "allow") return "deny";
4558
+ if (current === "plan" && tool.readOnly !== true) return "deny";
4516
4559
  if (configured === "allow") return "allow";
4517
4560
  if (bypassEnabled) return "allow";
4518
4561
  if (current === "accept" && isFileEdit(tool)) return "allow";
@@ -4524,8 +4567,9 @@ function createPermissionEngine(configPermissions) {
4524
4567
  },
4525
4568
  denyReason(tool, requested) {
4526
4569
  const configured = resolve3(tool, requested);
4570
+ if (current === "plan" && tool.readOnly !== true) return PLAN_REFUSAL;
4527
4571
  if (configured === "deny" || configured === "allow") return void 0;
4528
- return current === "plan" ? PLAN_REFUSAL : void 0;
4572
+ return void 0;
4529
4573
  },
4530
4574
  bypass: {
4531
4575
  enable() {
@@ -4555,7 +4599,133 @@ function isFileEdit(tool) {
4555
4599
  }
4556
4600
 
4557
4601
  // src/tools/edit.ts
4558
- import { lstat as lstat3, readFile as readFile8, stat as stat6, writeFile as writeFile6 } from "fs/promises";
4602
+ import { readFile as readFile8, stat as stat6 } from "fs/promises";
4603
+
4604
+ // src/tools/safe-write.ts
4605
+ import { randomUUID as randomUUID3 } from "crypto";
4606
+ import { chmod as chmod5, lstat as lstat3, open, rename as rename5, rm as rm3 } from "fs/promises";
4607
+ import { basename as basename2, dirname as dirname2, join } from "path";
4608
+ var UnsafeFileChangeError = class extends Error {
4609
+ constructor(message) {
4610
+ super(message);
4611
+ this.name = "UnsafeFileChangeError";
4612
+ }
4613
+ };
4614
+ async function readSafeFileSnapshot(file, maxBytes) {
4615
+ const parent = await lstat3(dirname2(file));
4616
+ if (parent.isSymbolicLink() || !parent.isDirectory()) {
4617
+ throw new UnsafeFileChangeError("the parent path is not a real directory");
4618
+ }
4619
+ const parentIdentity = directoryIdentity(parent);
4620
+ const pathInfo = await lstatMaybe(file);
4621
+ if (!pathInfo) return { exists: false, data: null, mode: 438, parent: parentIdentity };
4622
+ if (pathInfo.isSymbolicLink()) {
4623
+ throw new UnsafeFileChangeError("the path is a symbolic link");
4624
+ }
4625
+ if (!pathInfo.isFile()) throw new UnsafeFileChangeError("the path is not a regular file");
4626
+ if (pathInfo.size > maxBytes) throw new UnsafeFileChangeError("the file is too large");
4627
+ const handle = await open(file, "r");
4628
+ try {
4629
+ const opened = await handle.stat();
4630
+ if (!opened.isFile() || !sameObject(pathInfo, opened)) {
4631
+ throw new UnsafeFileChangeError("the file changed while it was being opened");
4632
+ }
4633
+ const data = await readBounded(handle, maxBytes);
4634
+ const afterRead = await handle.stat();
4635
+ if (!sameVersion(opened, afterRead)) {
4636
+ throw new UnsafeFileChangeError("the file changed while it was being read");
4637
+ }
4638
+ return {
4639
+ exists: true,
4640
+ data,
4641
+ mode: opened.mode & 511,
4642
+ identity: fileIdentity(opened),
4643
+ parent: parentIdentity
4644
+ };
4645
+ } finally {
4646
+ await handle.close();
4647
+ }
4648
+ }
4649
+ async function atomicWriteSafeFile(file, data, snapshot) {
4650
+ const parent = dirname2(file);
4651
+ await assertSameParent(parent, snapshot.parent);
4652
+ const temp = join(parent, `.${basename2(file)}.${process.pid}.${randomUUID3()}.tmp`);
4653
+ let handle;
4654
+ try {
4655
+ handle = await open(temp, "wx", snapshot.mode);
4656
+ await handle.writeFile(data, typeof data === "string" ? { encoding: "utf8" } : void 0);
4657
+ await handle.sync();
4658
+ await handle.close();
4659
+ handle = void 0;
4660
+ await chmod5(temp, snapshot.mode);
4661
+ await assertSameParent(parent, snapshot.parent);
4662
+ const current = await lstatMaybe(file);
4663
+ if (!matchesSnapshot2(current, snapshot)) {
4664
+ throw new UnsafeFileChangeError("the destination changed before it could be replaced");
4665
+ }
4666
+ await rename5(temp, file);
4667
+ } catch (error) {
4668
+ await handle?.close().catch(() => void 0);
4669
+ await rm3(temp, { force: true }).catch(() => void 0);
4670
+ throw error;
4671
+ }
4672
+ }
4673
+ async function readBounded(handle, maxBytes) {
4674
+ const chunks = [];
4675
+ let total = 0;
4676
+ for (; ; ) {
4677
+ const remaining = maxBytes + 1 - total;
4678
+ if (remaining <= 0) throw new UnsafeFileChangeError("the file is too large");
4679
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
4680
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
4681
+ if (bytesRead === 0) return Buffer.concat(chunks, total);
4682
+ chunks.push(chunk.subarray(0, bytesRead));
4683
+ total += bytesRead;
4684
+ }
4685
+ }
4686
+ async function assertSameParent(parent, expected) {
4687
+ const current = await lstat3(parent);
4688
+ if (current.isSymbolicLink() || !current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
4689
+ throw new UnsafeFileChangeError("the parent directory changed during the write");
4690
+ }
4691
+ }
4692
+ function matchesSnapshot2(current, snapshot) {
4693
+ if (!snapshot.exists) return current === null;
4694
+ return Boolean(
4695
+ current && !current.isSymbolicLink() && current.isFile() && sameIdentity(current, snapshot.identity)
4696
+ );
4697
+ }
4698
+ function sameObject(left, right) {
4699
+ return left.dev === right.dev && left.ino === right.ino;
4700
+ }
4701
+ function sameVersion(left, right) {
4702
+ return sameObject(left, right) && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
4703
+ }
4704
+ function sameIdentity(info, identity2) {
4705
+ return info.dev === identity2.dev && info.ino === identity2.ino && info.size === identity2.size && info.mtimeMs === identity2.mtimeMs && info.ctimeMs === identity2.ctimeMs;
4706
+ }
4707
+ function fileIdentity(info) {
4708
+ return {
4709
+ dev: info.dev,
4710
+ ino: info.ino,
4711
+ size: info.size,
4712
+ mtimeMs: info.mtimeMs,
4713
+ ctimeMs: info.ctimeMs
4714
+ };
4715
+ }
4716
+ function directoryIdentity(info) {
4717
+ return { dev: info.dev, ino: info.ino };
4718
+ }
4719
+ async function lstatMaybe(file) {
4720
+ try {
4721
+ return await lstat3(file);
4722
+ } catch (error) {
4723
+ if (error.code === "ENOENT") return null;
4724
+ throw error;
4725
+ }
4726
+ }
4727
+
4728
+ // src/tools/edit.ts
4559
4729
  var MAX_FILE_BYTES2 = 5e6;
4560
4730
  function countOccurrences(haystack, needle) {
4561
4731
  if (needle === "") return 0;
@@ -4611,19 +4781,14 @@ var editTool = {
4611
4781
  const { path: path14, oldString, newString, replaceAll = false } = input;
4612
4782
  const safe = resolveInside(ctx.cwd, path14);
4613
4783
  if (!safe.ok) return { content: safe.reason, isError: true };
4614
- const linkInfo = await lstat3(safe.path).catch(() => null);
4615
- if (linkInfo?.isSymbolicLink()) {
4616
- return { content: `Cannot edit ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
4617
- }
4618
- const info = await stat6(safe.path).catch(() => null);
4619
- if (info && info.size > MAX_FILE_BYTES2) {
4620
- return {
4621
- content: `Cannot edit ${path14}: the file exceeds the ${MAX_FILE_BYTES2 / 1e6} MB limit.`,
4622
- isError: true
4623
- };
4784
+ let snapshot;
4785
+ try {
4786
+ snapshot = await readSafeFileSnapshot(safe.path, MAX_FILE_BYTES2);
4787
+ } catch (error) {
4788
+ return { content: `Cannot edit ${path14}: ${error.message}.`, isError: true };
4624
4789
  }
4625
- const beforeBuffer = await readFile8(safe.path).catch(() => null);
4626
- if (beforeBuffer === null) return { content: `Cannot read ${path14}.`, isError: true };
4790
+ if (!snapshot.exists) return { content: `Cannot read ${path14}.`, isError: true };
4791
+ const beforeBuffer = snapshot.data;
4627
4792
  if (beforeBuffer.subarray(0, 8192).includes(0)) {
4628
4793
  return { content: `Cannot edit ${path14}: it is a binary file, not text.`, isError: true };
4629
4794
  }
@@ -4655,7 +4820,7 @@ var editTool = {
4655
4820
  return { content: `Checkpoint capture failed: ${error.message}`, isError: true };
4656
4821
  }
4657
4822
  try {
4658
- await writeFile6(safe.path, after, "utf8");
4823
+ await atomicWriteSafeFile(safe.path, after, snapshot);
4659
4824
  ctx.checkpoint?.markChanged(safe.path);
4660
4825
  } catch (error) {
4661
4826
  return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
@@ -4675,7 +4840,7 @@ function replacement(before, oldString, newString, replaceAll) {
4675
4840
  }
4676
4841
 
4677
4842
  // src/tools/glob.ts
4678
- import { join } from "path";
4843
+ import { join as join2 } from "path";
4679
4844
  import fg from "fast-glob";
4680
4845
  var MAX_RESULTS = 500;
4681
4846
  var IGNORED_DIRECTORIES = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
@@ -4692,6 +4857,7 @@ var globTool = {
4692
4857
  additionalProperties: false
4693
4858
  },
4694
4859
  defaultPermission: "allow",
4860
+ readOnly: true,
4695
4861
  summarize(input) {
4696
4862
  return `glob(${brief(input.pattern)})`;
4697
4863
  },
@@ -4713,7 +4879,7 @@ var globTool = {
4713
4879
  });
4714
4880
  if (entries.length === 0) return { content: `No files matched ${pattern}` };
4715
4881
  entries.sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0));
4716
- const paths = entries.slice(0, MAX_RESULTS).map((entry) => join(safe.relative, entry.path));
4882
+ const paths = entries.slice(0, MAX_RESULTS).map((entry) => join2(safe.relative, entry.path));
4717
4883
  if (entries.length > MAX_RESULTS) {
4718
4884
  paths.push(`... truncated: ${entries.length - MAX_RESULTS} more files matched.`);
4719
4885
  }
@@ -4723,7 +4889,7 @@ var globTool = {
4723
4889
 
4724
4890
  // src/tools/grep.ts
4725
4891
  import { readFile as readFile9, stat as stat7 } from "fs/promises";
4726
- import { join as join2 } from "path";
4892
+ import { join as join3 } from "path";
4727
4893
  import fg2 from "fast-glob";
4728
4894
 
4729
4895
  // src/tools/sensitive.ts
@@ -4872,6 +5038,7 @@ var grepTool = {
4872
5038
  additionalProperties: false
4873
5039
  },
4874
5040
  defaultPermission: "allow",
5041
+ readOnly: true,
4875
5042
  permission(input, ctx) {
4876
5043
  const { path: path14, glob } = input ?? {};
4877
5044
  if (isSensitivePath(path14) || mentionsSensitivePattern(glob)) return "ask";
@@ -4910,8 +5077,8 @@ var grepTool = {
4910
5077
  ignore: IGNORED_DIRECTORIES,
4911
5078
  stats: true
4912
5079
  })).sort((a, b) => (b.stats?.mtimeMs ?? 0) - (a.stats?.mtimeMs ?? 0)).map((entry) => ({
4913
- absolute: join2(safe.path, entry.path),
4914
- relative: join2(safe.relative, entry.path),
5080
+ absolute: join3(safe.path, entry.path),
5081
+ relative: join3(safe.relative, entry.path),
4915
5082
  size: entry.stats?.size ?? 0
4916
5083
  })) : [{ absolute: safe.path, relative: safe.relative, size: target.size }];
4917
5084
  const files = discovered.slice(0, MAX_FILES);
@@ -4979,6 +5146,7 @@ var readTool = {
4979
5146
  additionalProperties: false
4980
5147
  },
4981
5148
  defaultPermission: "allow",
5149
+ readOnly: true,
4982
5150
  permission(input, ctx) {
4983
5151
  const target = input?.path;
4984
5152
  if (isSensitivePath(target)) return "ask";
@@ -5039,8 +5207,8 @@ function toToolSchema(tool) {
5039
5207
  }
5040
5208
 
5041
5209
  // src/tools/write.ts
5042
- import { lstat as lstat4, mkdir as mkdir4, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
5043
- import { dirname as dirname2 } from "path";
5210
+ import { mkdir as mkdir4, readFile as readFile11, stat as stat9 } from "fs/promises";
5211
+ import { dirname as dirname3 } from "path";
5044
5212
  var MAX_FILE_BYTES5 = 5e6;
5045
5213
  var writeTool = {
5046
5214
  name: "write",
@@ -5090,31 +5258,29 @@ var writeTool = {
5090
5258
  }
5091
5259
  const safe = resolveInside(ctx.cwd, path14);
5092
5260
  if (!safe.ok) return { content: safe.reason, isError: true };
5093
- const linkInfo = await lstat4(safe.path).catch(() => null);
5094
- if (linkInfo?.isSymbolicLink()) {
5095
- return { content: `Cannot write ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
5096
- }
5097
- const beforeInfo = await stat9(safe.path).catch(() => null);
5098
- if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
5099
- return {
5100
- content: `Cannot replace ${path14}: the existing file exceeds the ${MAX_FILE_BYTES5 / 1e6} MB limit.`,
5101
- isError: true
5102
- };
5103
- }
5104
- const beforeBuffer = await readFile11(safe.path).catch(() => null);
5105
- if (beforeBuffer instanceof Buffer && beforeBuffer.subarray(0, 8192).includes(0)) {
5106
- return { content: `Cannot write ${path14}: the existing file is binary, not text.`, isError: true };
5107
- }
5108
- let before = "";
5109
- if (beforeBuffer) before = beforeBuffer.toString("utf8");
5261
+ let target = safe.path;
5262
+ let snapshot;
5110
5263
  try {
5111
- await mkdir4(dirname2(safe.path), { recursive: true });
5112
- await ctx.checkpoint?.capture(safe.path);
5113
- await writeFile7(safe.path, content, "utf8");
5114
- ctx.checkpoint?.markChanged(safe.path);
5264
+ await mkdir4(dirname3(target), { recursive: true });
5265
+ const rechecked = resolveInside(ctx.cwd, path14);
5266
+ if (!rechecked.ok || rechecked.path !== target) {
5267
+ return {
5268
+ content: `Cannot write ${path14}: the path changed while its parent was created.`,
5269
+ isError: true
5270
+ };
5271
+ }
5272
+ target = rechecked.path;
5273
+ snapshot = await readSafeFileSnapshot(target, MAX_FILE_BYTES5);
5274
+ if (snapshot.exists && snapshot.data.subarray(0, 8192).includes(0)) {
5275
+ return { content: `Cannot write ${path14}: the existing file is binary, not text.`, isError: true };
5276
+ }
5277
+ await ctx.checkpoint?.capture(target);
5278
+ await atomicWriteSafeFile(target, content, snapshot);
5279
+ ctx.checkpoint?.markChanged(target);
5115
5280
  } catch (error) {
5116
5281
  return { content: `Failed to write ${path14}: ${error.message}`, isError: true };
5117
5282
  }
5283
+ const before = snapshot.exists ? snapshot.data.toString("utf8") : "";
5118
5284
  const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
5119
5285
  return {
5120
5286
  content: `${before === "" ? "Created" : "Updated"} ${path14} (${lines} ${lines === 1 ? "line" : "lines"})`,
@@ -5147,7 +5313,7 @@ function createToolRegistry(tools) {
5147
5313
  }
5148
5314
 
5149
5315
  // src/prompts/library.ts
5150
- import { chmod as chmod5, readFile as readFile12, readdir as readdir3, unlink as unlink3, writeFile as writeFile8 } from "fs/promises";
5316
+ import { chmod as chmod6, readFile as readFile12, readdir as readdir3, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
5151
5317
  import path10 from "path";
5152
5318
  async function savePrompt(input) {
5153
5319
  const slug = slugify(input.name);
@@ -5161,11 +5327,11 @@ async function savePrompt(input) {
5161
5327
  };
5162
5328
  await ensureDir(promptsDir);
5163
5329
  const file = path10.join(promptsDir, `${slug}.md`);
5164
- await writeFile8(file, serialize(prompt), {
5330
+ await writeFile6(file, serialize(prompt), {
5165
5331
  encoding: "utf8",
5166
5332
  mode: 384
5167
5333
  });
5168
- await chmod5(file, 384);
5334
+ await chmod6(file, 384);
5169
5335
  return prompt;
5170
5336
  }
5171
5337
  async function getPrompt(slug) {
@@ -5252,18 +5418,21 @@ function parse(slug, text) {
5252
5418
  }
5253
5419
 
5254
5420
  // src/skills/library.ts
5255
- import { open, readFile as readFile13, readdir as readdir4, stat as stat10 } from "fs/promises";
5421
+ import { lstat as lstat4, open as open2, readdir as readdir4 } from "fs/promises";
5256
5422
  import path11 from "path";
5257
5423
  var SKILL_FILE = "SKILL.md";
5258
5424
  var FRONTMATTER_BYTES = 8192;
5259
5425
  var MAX_SKILL_BYTES = 5e6;
5260
5426
  var MAX_SKILLS_PER_ROOT = 500;
5427
+ var guards = /* @__PURE__ */ new WeakMap();
5261
5428
  async function discoverSkills(dirs) {
5262
5429
  const byName = /* @__PURE__ */ new Map();
5263
5430
  for (const root of dirs) {
5264
- const entries = await readdir4(root).catch(() => []);
5431
+ const rootInfo = await lstat4(root).catch(() => null);
5432
+ if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
5433
+ const entries = await readdir4(root, { withFileTypes: true }).catch(() => []);
5265
5434
  const found = await Promise.all(
5266
- entries.slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(path11.join(root, entry)))
5435
+ entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path11.join(root, entry.name)))
5267
5436
  );
5268
5437
  for (const meta of found) {
5269
5438
  if (meta && !byName.has(meta.name)) byName.set(meta.name, meta);
@@ -5272,11 +5441,25 @@ async function discoverSkills(dirs) {
5272
5441
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
5273
5442
  }
5274
5443
  async function loadSkill(meta) {
5275
- const info = await stat10(meta.file);
5276
- if (info.size > MAX_SKILL_BYTES) {
5444
+ const guard = guards.get(meta);
5445
+ if (!guard) throw new Error(`Skill was not discovered through a protected skill root: ${meta.file}`);
5446
+ await assertIdentity(guard.root, guard.rootIdentity, "skill root");
5447
+ await assertIdentity(meta.dir, guard.dirIdentity, "skill directory");
5448
+ const opened = await openVerified(meta.file, guard.fileIdentity);
5449
+ if (opened.info.size > MAX_SKILL_BYTES) {
5450
+ await opened.handle.close();
5277
5451
  throw new Error(`Skill file exceeds the ${MAX_SKILL_BYTES / 1e6} MB limit: ${meta.file}`);
5278
5452
  }
5279
- const { body } = parseFrontmatter(await readFile13(meta.file, "utf8"));
5453
+ let text;
5454
+ try {
5455
+ text = (await readBounded2(opened.handle, MAX_SKILL_BYTES)).toString("utf8");
5456
+ } finally {
5457
+ await opened.handle.close();
5458
+ }
5459
+ await assertIdentity(guard.root, guard.rootIdentity, "skill root");
5460
+ await assertIdentity(meta.dir, guard.dirIdentity, "skill directory");
5461
+ await assertIdentity(meta.file, guard.fileIdentity, "skill file");
5462
+ const { body } = parseFrontmatter(text);
5280
5463
  return { ...meta, body };
5281
5464
  }
5282
5465
  function formatSkillCatalogue(skills) {
@@ -5286,20 +5469,33 @@ function formatSkillCatalogue(skills) {
5286
5469
  "\n"
5287
5470
  );
5288
5471
  }
5289
- async function readMeta(dir) {
5472
+ async function readMeta(root, rootInfo, dir) {
5473
+ const dirInfo = await lstat4(dir).catch(() => null);
5474
+ if (!dirInfo?.isDirectory() || dirInfo.isSymbolicLink()) return null;
5290
5475
  const file = path11.join(dir, SKILL_FILE);
5291
5476
  const head = await readFrontmatterBytes(file);
5292
- if (head === null) return null;
5293
- const { fields } = parseFrontmatter(head);
5294
- return { name: fields.name || path11.basename(dir), description: fields.description ?? "", dir, file };
5477
+ if (!head) return null;
5478
+ const { fields } = parseFrontmatter(head.text);
5479
+ const meta = { name: fields.name || path11.basename(dir), description: fields.description ?? "", dir, file };
5480
+ guards.set(meta, {
5481
+ root,
5482
+ rootIdentity: identity(rootInfo),
5483
+ dirIdentity: identity(dirInfo),
5484
+ fileIdentity: head.identity
5485
+ });
5486
+ return meta;
5295
5487
  }
5296
5488
  async function readFrontmatterBytes(file) {
5297
- const handle = await open(file, "r").catch(() => null);
5489
+ const before = await lstat4(file).catch(() => null);
5490
+ if (!before?.isFile() || before.isSymbolicLink()) return null;
5491
+ const handle = await open2(file, "r").catch(() => null);
5298
5492
  if (!handle) return null;
5299
5493
  try {
5494
+ const opened = await handle.stat();
5495
+ if (!opened.isFile() || !sameIdentity2(opened, identity(before))) return null;
5300
5496
  const buffer = Buffer.alloc(FRONTMATTER_BYTES);
5301
5497
  const { bytesRead } = await handle.read(buffer, 0, FRONTMATTER_BYTES, 0);
5302
- return buffer.subarray(0, bytesRead).toString("utf8");
5498
+ return { text: buffer.subarray(0, bytesRead).toString("utf8"), identity: identity(opened) };
5303
5499
  } catch {
5304
5500
  return null;
5305
5501
  } finally {
@@ -5307,6 +5503,44 @@ async function readFrontmatterBytes(file) {
5307
5503
  });
5308
5504
  }
5309
5505
  }
5506
+ async function openVerified(file, expected) {
5507
+ const before = await lstat4(file);
5508
+ if (!before.isFile() || before.isSymbolicLink() || !sameIdentity2(before, expected)) {
5509
+ throw new Error(`Refusing changed or symlinked skill file: ${file}`);
5510
+ }
5511
+ const handle = await open2(file, "r");
5512
+ const info = await handle.stat();
5513
+ if (!info.isFile() || !sameIdentity2(info, expected)) {
5514
+ await handle.close();
5515
+ throw new Error(`Refusing skill file changed while opening: ${file}`);
5516
+ }
5517
+ return { handle, info };
5518
+ }
5519
+ async function assertIdentity(file, expected, label) {
5520
+ const info = await lstat4(file);
5521
+ if (info.isSymbolicLink() || !sameIdentity2(info, expected)) {
5522
+ throw new Error(`Refusing changed or symlinked ${label}: ${file}`);
5523
+ }
5524
+ }
5525
+ async function readBounded2(handle, maxBytes) {
5526
+ const chunks = [];
5527
+ let total = 0;
5528
+ for (; ; ) {
5529
+ const remaining = maxBytes + 1 - total;
5530
+ if (remaining <= 0) throw new Error(`Skill file exceeds the ${maxBytes / 1e6} MB limit`);
5531
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remaining));
5532
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
5533
+ if (bytesRead === 0) return Buffer.concat(chunks, total);
5534
+ chunks.push(chunk.subarray(0, bytesRead));
5535
+ total += bytesRead;
5536
+ }
5537
+ }
5538
+ function identity(info) {
5539
+ return { dev: info.dev, ino: info.ino };
5540
+ }
5541
+ function sameIdentity2(info, expected) {
5542
+ return info.dev === expected.dev && info.ino === expected.ino;
5543
+ }
5310
5544
  function parseFrontmatter(text) {
5311
5545
  const lines = text.split("\n");
5312
5546
  const fields = {};
@@ -5325,7 +5559,7 @@ function parseFrontmatter(text) {
5325
5559
  }
5326
5560
 
5327
5561
  // src/skills/install.ts
5328
- import { randomUUID as randomUUID3 } from "crypto";
5562
+ import { randomUUID as randomUUID4 } from "crypto";
5329
5563
  import { execFileSync } from "child_process";
5330
5564
  import {
5331
5565
  existsSync as existsSync2,
@@ -5337,7 +5571,7 @@ import {
5337
5571
  rmSync,
5338
5572
  writeFileSync
5339
5573
  } from "fs";
5340
- import { chmod as chmod6, mkdir as mkdir5 } from "fs/promises";
5574
+ import { chmod as chmod7, mkdir as mkdir5 } from "fs/promises";
5341
5575
  import path12 from "path";
5342
5576
  var TMP_DIR = path12.join(skillsDir, ".tmp");
5343
5577
  var MAX_SKILL_BYTES2 = 5e6;
@@ -5366,7 +5600,7 @@ async function installFromGitHub(url) {
5366
5600
  const { owner, repo, subdir, branch } = parseGitHubUrl(url);
5367
5601
  const name = safeSkillName(subdir ? path12.posix.basename(subdir) : repo);
5368
5602
  const skillDir = safeChildPath(skillsDir, name);
5369
- const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID3()}`);
5603
+ const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
5370
5604
  try {
5371
5605
  mkdirSync(tmpDir, { recursive: true });
5372
5606
  const cloneUrl = `https://github.com/${owner}/${repo}.git`;
@@ -5388,7 +5622,7 @@ async function installFromNpm(packageName) {
5388
5622
  if (!parsedName) throw new Error(`Invalid npm package name: ${packageName}`);
5389
5623
  const name = safeSkillName(parsedName);
5390
5624
  const skillDir = safeChildPath(skillsDir, name);
5391
- const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID3()}`);
5625
+ const tmpDir = safeChildPath(TMP_DIR, `${name}-${randomUUID4()}`);
5392
5626
  try {
5393
5627
  mkdirSync(tmpDir, { recursive: true });
5394
5628
  execFileSync("npm", ["pack", packageName, "--prefix", tmpDir], {
@@ -5492,7 +5726,7 @@ async function writeSkillFile(dir, body) {
5492
5726
  throw new Error(`Refusing to replace symlinked skill file: ${file}`);
5493
5727
  }
5494
5728
  writeFileSync(file, body, { encoding: "utf8", mode: 384 });
5495
- await chmod6(file, 384);
5729
+ await chmod7(file, 384);
5496
5730
  }
5497
5731
  function parseGitHubUrl(url) {
5498
5732
  let parsed;
@@ -5586,6 +5820,7 @@ function createSkillTool(skills) {
5586
5820
  additionalProperties: false
5587
5821
  },
5588
5822
  defaultPermission: "allow",
5823
+ readOnly: true,
5589
5824
  summarize(input) {
5590
5825
  return `skill(${brief(input.name)})`;
5591
5826
  },
@@ -5691,50 +5926,94 @@ function field(input, key) {
5691
5926
  }
5692
5927
 
5693
5928
  // src/core/update.ts
5694
- var UPDATE_URL = `https://api.github.com/repos/${KITCODE_REPOSITORY}/commits/main`;
5929
+ var UPDATE_URL = "https://registry.npmjs.org/%40kernelonpanic%2Fkitcode/latest";
5930
+ var PACKAGE_URL = "https://www.npmjs.com/package/@kernelonpanic/kitcode";
5695
5931
  var TIMEOUT_MS = 4e3;
5696
5932
  var MAX_RESPONSE_BYTES2 = 128e3;
5697
- async function checkForUpdates(fetcher = fetch, currentCommit = KITCODE_COMMIT) {
5698
- if (currentCommit === "development") {
5699
- return { status: "unknown", reason: "development build has no embedded commit" };
5933
+ async function checkForUpdates(fetcher = fetch, currentVersion = KITCODE_VERSION) {
5934
+ if (!parseSemver(currentVersion)) {
5935
+ return { status: "unknown", reason: "installed KitCode version is not valid semver" };
5700
5936
  }
5701
5937
  const controller = new AbortController();
5702
5938
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
5703
5939
  try {
5704
5940
  const response = await fetcher(UPDATE_URL, {
5705
5941
  headers: {
5706
- accept: "application/vnd.github+json",
5707
- "user-agent": `KitCode/${KITCODE_VERSION}`,
5708
- "x-github-api-version": "2022-11-28"
5942
+ accept: "application/json",
5943
+ "user-agent": `KitCode/${currentVersion}`
5709
5944
  },
5710
5945
  redirect: "error",
5711
5946
  signal: controller.signal
5712
5947
  });
5713
5948
  if (!response.ok) {
5714
5949
  await response.body?.cancel().catch(() => void 0);
5715
- return { status: "unknown", reason: `GitHub returned ${response.status}` };
5950
+ return { status: "unknown", reason: `npm registry returned ${response.status}` };
5716
5951
  }
5717
5952
  const payload = await readJsonBounded(response);
5718
- if (!payload) return { status: "unknown", reason: "GitHub response was too large or invalid" };
5719
- if (typeof payload.sha !== "string" || !/^[0-9a-f]{40}$/i.test(payload.sha)) {
5720
- return { status: "unknown", reason: "GitHub response did not contain a commit" };
5953
+ if (!payload) return { status: "unknown", reason: "npm response was too large or invalid" };
5954
+ if (typeof payload.version !== "string" || !parseSemver(payload.version)) {
5955
+ return { status: "unknown", reason: "npm response did not contain a valid version" };
5721
5956
  }
5722
- const current = currentCommit.toLowerCase();
5723
- const latest = payload.sha.toLowerCase();
5724
- if (latest.startsWith(current) || current.startsWith(latest)) {
5725
- return { status: "current", current, latest };
5957
+ const latest = payload.version;
5958
+ if (compareSemver(latest, currentVersion) <= 0) {
5959
+ return { status: "current", current: currentVersion, latest };
5726
5960
  }
5727
- const url = typeof payload.html_url === "string" && payload.html_url.startsWith("https://github.com/") ? payload.html_url : `https://github.com/${KITCODE_REPOSITORY}/commits/main`;
5728
- return { status: "available", current, latest, url };
5961
+ return {
5962
+ status: "available",
5963
+ current: currentVersion,
5964
+ latest,
5965
+ url: `${PACKAGE_URL}/v/${encodeURIComponent(latest)}`
5966
+ };
5729
5967
  } catch (error) {
5730
5968
  return {
5731
5969
  status: "unknown",
5732
- reason: error instanceof Error && error.name === "AbortError" ? "GitHub check timed out" : "GitHub check failed"
5970
+ reason: error instanceof Error && error.name === "AbortError" ? "npm update check timed out" : "npm update check failed"
5733
5971
  };
5734
5972
  } finally {
5735
5973
  clearTimeout(timer);
5736
5974
  }
5737
5975
  }
5976
+ function compareSemver(left, right) {
5977
+ const a = parseSemver(left);
5978
+ const b = parseSemver(right);
5979
+ if (!a || !b) throw new Error("Cannot compare invalid semantic versions");
5980
+ for (let index = 0; index < 3; index += 1) {
5981
+ const x = a.core[index] ?? 0n;
5982
+ const y = b.core[index] ?? 0n;
5983
+ if (x !== y) return x > y ? 1 : -1;
5984
+ }
5985
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
5986
+ if (a.prerelease.length === b.prerelease.length) return 0;
5987
+ return a.prerelease.length === 0 ? 1 : -1;
5988
+ }
5989
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
5990
+ for (let index = 0; index < length; index += 1) {
5991
+ const x = a.prerelease[index];
5992
+ const y = b.prerelease[index];
5993
+ if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
5994
+ if (x === y) continue;
5995
+ const xNumeric = /^\d+$/.test(x);
5996
+ const yNumeric = /^\d+$/.test(y);
5997
+ if (xNumeric && yNumeric) return BigInt(x) > BigInt(y) ? 1 : -1;
5998
+ if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
5999
+ return x > y ? 1 : -1;
6000
+ }
6001
+ return 0;
6002
+ }
6003
+ function parseSemver(value) {
6004
+ const match = value.match(
6005
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
6006
+ );
6007
+ if (!match) return null;
6008
+ const prerelease = match[4]?.split(".") ?? [];
6009
+ if (prerelease.some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith("0"))) {
6010
+ return null;
6011
+ }
6012
+ return {
6013
+ core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
6014
+ prerelease
6015
+ };
6016
+ }
5738
6017
  async function readJsonBounded(response) {
5739
6018
  const declared = Number(response.headers.get("content-length"));
5740
6019
  if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES2) {
@@ -5782,7 +6061,7 @@ async function boot(options) {
5782
6061
  const warnings = [];
5783
6062
  const workspaceRoot2 = await canonicalWorkspace(options.cwd);
5784
6063
  const workspaceTrusted = await isWorkspaceTrusted(options.cwd);
5785
- const startupUpdate = config.updates.checkOnStart ? checkForUpdates() : null;
6064
+ let startupUpdate;
5786
6065
  if (loadedConfig.ignoredProject) {
5787
6066
  warnings.push(
5788
6067
  `Project config ignored until this workspace is trusted: ${loadedConfig.ignoredProject.path}. Review it, then run: kitcode trust`
@@ -6395,7 +6674,7 @@ ${lines.join("\n")}` : null;
6395
6674
  const states = mcp.states();
6396
6675
  const lines = [
6397
6676
  `KitCode ${KITCODE_VERSION} \xB7 ${KITCODE_COMMIT.slice(0, 12)}`,
6398
- `updates: ${config.updates.checkOnStart ? "enabled" : "disabled"}`,
6677
+ "updates: npm check runs on every app start",
6399
6678
  `runtime: Node ${process.versions.node} \xB7 ${process.platform}/${process.arch}`,
6400
6679
  `workspace: ${options.cwd}`,
6401
6680
  `config: ${location.path}`,
@@ -6423,7 +6702,8 @@ ${lines.join("\n")}` : null;
6423
6702
  );
6424
6703
  return lines.join("\n");
6425
6704
  },
6426
- startupUpdateCheck: () => startupUpdate,
6705
+ startupUpdateCheck: () => startupUpdate ??= checkForUpdates(),
6706
+ checkForUpdates: () => checkForUpdates(),
6427
6707
  async run(history, hooks, signal) {
6428
6708
  syncMcpTools();
6429
6709
  const runProviderId = parseModelRef(modelRef)?.provider;
@@ -6561,6 +6841,13 @@ async function ask(text, options = {}) {
6561
6841
  mode: options.mode
6562
6842
  });
6563
6843
  for (const warning of warnings) console.error(sanitizeTerminalText(warning));
6844
+ const update = await runtime.startupUpdateCheck();
6845
+ if (update.status === "available") {
6846
+ console.error(
6847
+ `A newer KitCode version is available (${update.latest}). Update with: npm install -g @kernelonpanic/kitcode@latest
6848
+ ${update.url}`
6849
+ );
6850
+ }
6564
6851
  const controller = new AbortController();
6565
6852
  const onInterrupt = () => controller.abort();
6566
6853
  process.on("SIGINT", onInterrupt);
@@ -6730,7 +7017,7 @@ function validateKey(value) {
6730
7017
  import { render } from "ink";
6731
7018
 
6732
7019
  // src/ui/App.tsx
6733
- import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize2 } from "ink";
7020
+ import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize4 } from "ink";
6734
7021
  import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
6735
7022
 
6736
7023
  // src/mcp/add.ts
@@ -6784,6 +7071,7 @@ var COMMANDS = [
6784
7071
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
6785
7072
  { name: "attach", args: "<path|clipboard|clear>" },
6786
7073
  { name: "compact" },
7074
+ { name: "update" },
6787
7075
  { name: "checker" },
6788
7076
  { name: "sessions", args: "[list|rename|delete [all]|export]" },
6789
7077
  { name: "config" },
@@ -7026,7 +7314,11 @@ var en = {
7026
7314
  sessionActionRename: "rename",
7027
7315
  sessionActionDelete: "delete",
7028
7316
  sessionActionExport: "export Markdown",
7029
- updateAvailable: (version, url) => `A newer KitCode build is available (${version}): ${url}`,
7317
+ updateAvailable: (version, url) => `A newer KitCode version is available (${version}). Update with:
7318
+ npm install -g @kernelonpanic/kitcode@latest
7319
+ ${url}`,
7320
+ updateCurrent: (version) => `KitCode ${version} is up to date.`,
7321
+ updateFailed: (reason) => `Could not check for updates: ${reason}.`,
7030
7322
  accentSet: (name, hex) => `Accent: ${name} (${hex})`,
7031
7323
  reasoning: (on) => `Reasoning ${on ? "on" : "off"}`,
7032
7324
  effortSet: (value) => `Effort: ${value}`,
@@ -7101,6 +7393,7 @@ var en = {
7101
7393
  "mcp disable": "disconnect without removing an MCP server",
7102
7394
  attach: "attach an image or text file to the next message",
7103
7395
  compact: "summarize older context and keep recent turns",
7396
+ update: "check npm for a newer KitCode version",
7104
7397
  checker: "check local setup without spending model tokens",
7105
7398
  sessions: "search and manage saved sessions",
7106
7399
  "sessions list": "list saved sessions",
@@ -7209,7 +7502,11 @@ var ru = {
7209
7502
  sessionActionRename: "\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",
7210
7503
  sessionActionDelete: "\u0443\u0434\u0430\u043B\u0438\u0442\u044C",
7211
7504
  sessionActionExport: "\u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C Markdown",
7212
- updateAvailable: (version, url) => `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F KitCode (${version}): ${url}`,
7505
+ updateAvailable: (version, url) => `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F KitCode (${version}). \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C:
7506
+ npm install -g @kernelonpanic/kitcode@latest
7507
+ ${url}`,
7508
+ updateCurrent: (version) => `\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F KitCode ${version}.`,
7509
+ updateFailed: (reason) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F: ${reason}.`,
7213
7510
  accentSet: (name, hex) => `\u0426\u0432\u0435\u0442: ${name} (${hex})`,
7214
7511
  reasoning: (on) => `\u0420\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u044F ${on ? "\u0432\u043A\u043B\u044E\u0447\u0435\u043D\u044B" : "\u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u044B"}`,
7215
7512
  effortSet: (value) => `\u0413\u043B\u0443\u0431\u0438\u043D\u0430: ${value}`,
@@ -7272,6 +7569,7 @@ var ru = {
7272
7569
  lang: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u044F\u0437\u044B\u043A \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430",
7273
7570
  prompt: "\u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442",
7274
7571
  skills: "\u0441\u043F\u0438\u0441\u043E\u043A \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u043A\u0438\u043B\u043B\u043E\u0432",
7572
+ "prompt delete": "\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0439 \u043F\u0440\u043E\u043C\u0442",
7275
7573
  bypass: "\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F (\u0441\u043F\u0440\u043E\u0441\u0438\u0442 \u0434\u0432\u0430\u0436\u0434\u044B)",
7276
7574
  usage: "\u0442\u043E\u043A\u0435\u043D\u044B \u0438 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C",
7277
7575
  mcp: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0438\u043B\u0438 \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C MCP-\u0441\u0435\u0440\u0432\u0435\u0440",
@@ -7282,6 +7580,7 @@ var ru = {
7282
7580
  "mcp disable": "\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C MCP \u0431\u0435\u0437 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F",
7283
7581
  attach: "\u043F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u0438\u043B\u0438 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u043A \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044E",
7284
7582
  compact: "\u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0439 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442, \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0432 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 \u0445\u043E\u0434\u044B",
7583
+ update: "\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0432\u0435\u0440\u0441\u0438\u044E KitCode \u0432 npm",
7285
7584
  checker: "\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443 \u0431\u0435\u0437 \u0442\u0440\u0430\u0442\u044B \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438",
7286
7585
  sessions: "\u043F\u043E\u0438\u0441\u043A \u0438 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u043C\u0438 \u0441\u0435\u0441\u0441\u0438\u044F\u043C\u0438",
7287
7586
  "sessions list": "\u0441\u043F\u0438\u0441\u043E\u043A \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439",
@@ -7764,7 +8063,7 @@ var PromptInput = memo(function PromptInput2({
7764
8063
  valueRef.current = safeValue;
7765
8064
  cursorRef.current = inputCursor;
7766
8065
  const suggestions = matchCommands(safeValue);
7767
- const open2 = suggestions.length > 0;
8066
+ const open3 = suggestions.length > 0;
7768
8067
  const active2 = Math.min(selectionCursor, Math.max(0, suggestions.length - 1));
7769
8068
  useEffect(() => {
7770
8069
  setSelectionCursor(0);
@@ -7817,25 +8116,25 @@ var PromptInput = memo(function PromptInput2({
7817
8116
  onPasteImage();
7818
8117
  return;
7819
8118
  }
7820
- if (open2 && key.upArrow) {
8119
+ if (open3 && key.upArrow) {
7821
8120
  setSelectionCursor(Math.max(0, active2 - 1));
7822
8121
  return;
7823
8122
  }
7824
- if (open2 && key.downArrow) {
8123
+ if (open3 && key.downArrow) {
7825
8124
  setSelectionCursor(Math.min(suggestions.length - 1, active2 + 1));
7826
8125
  return;
7827
8126
  }
7828
- if (open2 && key.tab && !key.shift) {
8127
+ if (open3 && key.tab && !key.shift) {
7829
8128
  const chosen = suggestions[active2];
7830
8129
  if (chosen) change(`/${chosen.name} `);
7831
8130
  return;
7832
8131
  }
7833
- if (open2 && key.return) {
8132
+ if (open3 && key.return) {
7834
8133
  const chosen = suggestions[active2];
7835
8134
  if (chosen) submit(`/${chosen.name}`);
7836
8135
  return;
7837
8136
  }
7838
- if (!open2 && (key.upArrow || key.downArrow)) {
8137
+ if (!open3 && (key.upArrow || key.downArrow)) {
7839
8138
  const moved = moveInputHistory(
7840
8139
  history,
7841
8140
  historyIndex,
@@ -7887,7 +8186,7 @@ var PromptInput = memo(function PromptInput2({
7887
8186
  });
7888
8187
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
7889
8188
  const visible = suggestions.slice(start, start + WINDOW2);
7890
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
8189
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
7891
8190
  /* @__PURE__ */ jsxs8(
7892
8191
  Box8,
7893
8192
  {
@@ -7915,7 +8214,7 @@ var PromptInput = memo(function PromptInput2({
7915
8214
  " ",
7916
8215
  sanitizeTerminalText(hint)
7917
8216
  ] }),
7918
- open2 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
8217
+ open3 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
7919
8218
  visible.map((command, index) => {
7920
8219
  const selected = start + index === active2;
7921
8220
  return /* @__PURE__ */ jsxs8(Text8, { color: selected ? theme.accent : void 0, dimColor: !selected, children: [
@@ -8051,6 +8350,7 @@ function StatusBar({ status }) {
8051
8350
  Box9,
8052
8351
  {
8053
8352
  width: "100%",
8353
+ flexShrink: 0,
8054
8354
  marginTop: 1,
8055
8355
  paddingX: 1,
8056
8356
  borderStyle: "single",
@@ -8182,8 +8482,12 @@ function ContextMeter({
8182
8482
  import { Box as Box10 } from "ink";
8183
8483
  import { jsx as jsx10 } from "react/jsx-runtime";
8184
8484
  function interactiveViewportRows(rows) {
8185
- if (!Number.isFinite(rows)) return 23;
8186
- return Math.max(1, Math.floor(rows) - 1);
8485
+ if (!Number.isFinite(rows)) return 22;
8486
+ return Math.max(1, Math.floor(rows) - 2);
8487
+ }
8488
+ var INTERACTIVE_CHROME_ROWS = 14;
8489
+ function liveTranscriptRows(rows) {
8490
+ return Math.max(1, interactiveViewportRows(rows) - INTERACTIVE_CHROME_ROWS);
8187
8491
  }
8188
8492
  function TerminalViewport({ children, rows }) {
8189
8493
  return /* @__PURE__ */ jsx10(
@@ -8198,14 +8502,16 @@ function TerminalViewport({ children, rows }) {
8198
8502
  }
8199
8503
 
8200
8504
  // src/ui/components/Transcript.tsx
8201
- import { Box as Box12, Static, Text as Text11 } from "ink";
8505
+ import { Box as Box12, Static, Text as Text11, useWindowSize as useWindowSize3 } from "ink";
8202
8506
  import { memo as memo2, useMemo as useMemo3, useRef as useRef3 } from "react";
8203
8507
  import Spinner2 from "ink-spinner";
8508
+ import stringWidth2 from "string-width";
8204
8509
 
8205
8510
  // src/ui/markdown.tsx
8206
- import { Box as Box11, Text as Text10 } from "ink";
8207
- import { useMemo as useMemo2 } from "react";
8208
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8511
+ import { Box as Box11, Text as Text10, useWindowSize as useWindowSize2 } from "ink";
8512
+ import { Fragment as Fragment4, useMemo as useMemo2 } from "react";
8513
+ import stringWidth from "string-width";
8514
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8209
8515
  function Markdown({ children }) {
8210
8516
  const blocks = useMemo2(() => extractBlocks(children), [children]);
8211
8517
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
@@ -8213,45 +8519,43 @@ function Markdown({ children }) {
8213
8519
  function BlockView({ block }) {
8214
8520
  switch (block.type) {
8215
8521
  case "heading": {
8216
- const sizes = [22, 20, 18, 16, 14, 13];
8217
- const size = sizes[(block.level ?? 1) - 1] ?? 14;
8218
- return /* @__PURE__ */ jsx11(Box11, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx11(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
8522
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(block.text ?? "") }) });
8219
8523
  }
8220
8524
  case "blockquote":
8221
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginLeft: 2, children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
8222
- /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2502 " }),
8223
- /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: line })
8224
- ] }, i)) });
8525
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx11(Text10, { dimColor: true, italic: true, children: inline(line) }, i)) });
8225
8526
  case "ul":
8226
8527
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
8227
- /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2022 " }),
8228
- /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
8528
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: item.text?.match(/^[☑☐] /) ? "" : "\u2022 " }),
8529
+ /* @__PURE__ */ jsx11(Box11, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
8229
8530
  ] }, i)) });
8230
8531
  case "ol":
8231
8532
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
8232
- /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: `${i + 1}. ` }),
8233
- /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
8533
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: `${(block.start ?? 1) + i}. ` }),
8534
+ /* @__PURE__ */ jsx11(Box11, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx11(BlockView, { block: item }) })
8234
8535
  ] }, i)) });
8235
8536
  case "paragraph":
8236
- default:
8237
- return /* @__PURE__ */ jsx11(Text10, { children: inline(block.text ?? "") });
8537
+ default: {
8538
+ const content = /* @__PURE__ */ jsx11(Text10, { children: inline(block.text ?? "") });
8539
+ if (!block.children?.length) return content;
8540
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
8541
+ content,
8542
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: block.children.map((child, index) => /* @__PURE__ */ jsx11(BlockView, { block: child }, index)) })
8543
+ ] });
8544
+ }
8238
8545
  case "table":
8239
8546
  return /* @__PURE__ */ jsx11(TableView, { block });
8240
8547
  case "code":
8241
- return /* @__PURE__ */ jsx11(CodeBlock, { lang: block.lang, code: block.text ?? "" });
8548
+ return /* @__PURE__ */ jsx11(CodeBlock, { code: block.text ?? "" });
8242
8549
  case "hr":
8243
- return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "\u2500".repeat(60) }) });
8550
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2500".repeat(60) }) });
8244
8551
  }
8245
8552
  }
8246
- function CodeBlock({ lang, code }) {
8247
- const theme = useTheme();
8553
+ function CodeBlock({ code }) {
8248
8554
  const lines = code.split("\n");
8249
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
8250
- lang && /* @__PURE__ */ jsx11(Text10, { dimColor: true, color: theme.accent, children: lang }),
8251
- /* @__PURE__ */ jsx11(Box11, { borderColor: "gray", borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) }) })
8252
- ] });
8555
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) });
8253
8556
  }
8254
8557
  function TableView({ block }) {
8558
+ const { columns } = useWindowSize2();
8255
8559
  const headers = block.headers ?? [];
8256
8560
  const rows = block.rows ?? [];
8257
8561
  if (headers.length === 0 && rows.length === 0) {
@@ -8261,77 +8565,104 @@ function TableView({ block }) {
8261
8565
  if (colCount === 0) {
8262
8566
  return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(empty table)" });
8263
8567
  }
8264
- const colWidths = new Array(colCount).fill(0);
8265
- const allRows = [headers, ...rows];
8568
+ const allRows = [headers, ...rows].map(
8569
+ (row) => Array.from({ length: colCount }, (_, index) => inlineDisplayText(row[index] ?? ""))
8570
+ );
8571
+ const naturalWidths = new Array(colCount).fill(1);
8266
8572
  for (const row of allRows) {
8267
8573
  for (let i = 0; i < colCount; i++) {
8268
8574
  const cell = row[i] ?? "";
8269
- colWidths[i] = Math.max(colWidths[i], [...cell].length);
8575
+ naturalWidths[i] = Math.max(naturalWidths[i] ?? 1, stringWidth(cell));
8270
8576
  }
8271
8577
  }
8272
- const separator = colWidths.map((w) => "\u2500".repeat(w)).join("\u253C");
8578
+ const colWidths = fitColumnWidths(naturalWidths, Math.max(1, columns));
8579
+ const separator = colWidths.map((width, index) => {
8580
+ const edge = index === 0 || index === colWidths.length - 1;
8581
+ return "\u2500".repeat(width + (edge ? 1 : 2));
8582
+ }).join("\u253C");
8273
8583
  const lines = [];
8274
8584
  allRows.forEach((row, rowIdx) => {
8275
8585
  const cells = [];
8276
8586
  for (let i = 0; i < colCount; i++) {
8277
8587
  const cell = row[i] ?? "";
8278
8588
  const align = block.colAligns?.[i] ?? "left";
8279
- const visualWidth = [...cell].length;
8280
- const padWidth = colWidths[i] + (cell.length - visualWidth);
8589
+ const width = colWidths[i] ?? 1;
8590
+ const fitted = truncateToWidth(cell, width);
8591
+ const visualWidth = stringWidth(fitted);
8592
+ const totalPad = Math.max(0, width - visualWidth);
8281
8593
  let padded;
8282
8594
  if (align === "right") {
8283
- padded = cell.padStart(padWidth);
8595
+ padded = `${" ".repeat(totalPad)}${fitted}`;
8284
8596
  } else if (align === "center") {
8285
- const totalPad = padWidth - visualWidth;
8286
8597
  const left = Math.floor(totalPad / 2);
8287
- padded = " ".repeat(left) + cell + " ".repeat(totalPad - left);
8598
+ padded = `${" ".repeat(left)}${fitted}${" ".repeat(totalPad - left)}`;
8288
8599
  } else {
8289
- padded = cell.padEnd(padWidth);
8600
+ padded = `${fitted}${" ".repeat(totalPad)}`;
8290
8601
  }
8291
8602
  cells.push(padded);
8292
8603
  }
8293
8604
  lines.push(
8294
- /* @__PURE__ */ jsx11(Text10, { bold: rowIdx === 0, children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
8605
+ /* @__PURE__ */ jsx11(Text10, { bold: rowIdx === 0, wrap: "truncate-end", children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
8295
8606
  );
8296
8607
  if (rowIdx === 0) {
8297
- lines.push(/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: separator }, `sep-${rowIdx}`));
8608
+ lines.push(/* @__PURE__ */ jsx11(Text10, { dimColor: true, wrap: "truncate-end", children: separator }, `sep-${rowIdx}`));
8298
8609
  }
8299
8610
  });
8300
8611
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: lines });
8301
8612
  }
8302
- var CHARS_PER_FONT_LEVEL = 3;
8303
- function truncateBySize(text, size) {
8304
- const maxLen = size * CHARS_PER_FONT_LEVEL;
8305
- return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
8306
- }
8307
- var HEADING_RE = /^(#{1,6})\s+(.*)$/;
8613
+ function fitColumnWidths(natural, maxLineWidth) {
8614
+ const widths = natural.map((width) => Math.max(1, width));
8615
+ const separatorWidth = Math.max(0, widths.length - 1) * 3;
8616
+ const available = Math.max(widths.length, maxLineWidth - separatorWidth);
8617
+ let excess = widths.reduce((sum, width) => sum + width, 0) - available;
8618
+ while (excess > 0) {
8619
+ const shrinkable = widths.map((width, index) => ({ width, index })).filter(({ width }) => width > 1);
8620
+ if (shrinkable.length === 0) break;
8621
+ const share = Math.max(1, Math.ceil(excess / shrinkable.length));
8622
+ for (const { index } of shrinkable) {
8623
+ const current = widths[index] ?? 1;
8624
+ const amount = Math.min(current - 1, share, excess);
8625
+ widths[index] = current - amount;
8626
+ excess -= amount;
8627
+ if (excess === 0) break;
8628
+ }
8629
+ }
8630
+ return widths;
8631
+ }
8632
+ var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
8633
+ function truncateToWidth(text, maxWidth) {
8634
+ if (stringWidth(text) <= maxWidth) return text;
8635
+ if (maxWidth <= 1) return "\u2026";
8636
+ let result = "";
8637
+ for (const { segment } of graphemeSegmenter.segment(text)) {
8638
+ if (stringWidth(result + segment) > maxWidth - 1) break;
8639
+ result += segment;
8640
+ }
8641
+ return `${result}\u2026`;
8642
+ }
8643
+ function inlineDisplayText(text) {
8644
+ return text.replace(/!\[([^\]]*)\]\(([^)]*)\)/g, "[$1]").replace(/\[([^\]]*)\]\(([^)]*)\)/g, "$1($2)").replace(/(`+)(.*?)\1/g, "$2").replace(/(\*\*|__|~~)(?=\S)(.*?\S)\1/g, "$2").replace(/(?<!\\)(\*|_)(?=\S)(.*?\S)\1/g, "$2").replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, "$1");
8645
+ }
8646
+ var HEADING_RE = /^ {0,3}(#{1,6})(?:[ \t]+(.*)|[ \t]*)$/;
8308
8647
  var UL_RE = /^([-*+])\s+(.*)$/;
8309
- var OL_RE = /^(\d+)\.\s+(.*)$/;
8648
+ var OL_RE = /^(\d{1,9})[.)]\s+(.*)$/;
8310
8649
  var QUOTE_RE = /^>\s?(.*)$/;
8311
- var TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)$/;
8312
- var INDENT_RE = /^(\s*)(.*)$/;
8313
- var HR_RE = /^([-*_])\1{2,}$/;
8314
- var FENCE_RE = /^(`{3,}|~{3,})(\w*)\s*$/;
8650
+ var TASK_CONTENT_RE = /^\[([ xX])\]\s+(.*)$/;
8651
+ var SETEXT_RE = /^ {0,3}(=+|-+)[ \t]*$/;
8652
+ var MAX_TABLE_COLUMNS = 20;
8653
+ var MAX_LIST_DEPTH = 20;
8315
8654
  function extractBlocks(src) {
8316
- const lines = src.replace(/\r\n/g, "\n").split("\n");
8655
+ const lines = src.replace(/\r\n?/g, "\n").split("\n");
8317
8656
  const blocks = [];
8318
8657
  let paragraph = [];
8319
- let list = null;
8320
8658
  let quote = [];
8321
8659
  let code = null;
8322
- let table = null;
8323
8660
  const flushParagraph = () => {
8324
8661
  if (paragraph.length) {
8325
- blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
8662
+ blocks.push({ type: "paragraph", text: joinParagraphLines(paragraph) });
8326
8663
  paragraph = [];
8327
8664
  }
8328
8665
  };
8329
- const flushList = () => {
8330
- if (list && list.items.length) {
8331
- blocks.push({ type: list.ordered ? "ol" : "ul", items: list.items });
8332
- list = null;
8333
- }
8334
- };
8335
8666
  const flushQuote = () => {
8336
8667
  if (quote.length) {
8337
8668
  blocks.push({ type: "blockquote", text: quote.join("\n") });
@@ -8346,238 +8677,360 @@ function extractBlocks(src) {
8346
8677
  };
8347
8678
  const flushAll = () => {
8348
8679
  flushParagraph();
8349
- flushList();
8350
8680
  flushQuote();
8351
8681
  flushCode();
8352
8682
  };
8353
- const flushTable = () => {
8354
- if (table && table.headers.length > 0) {
8355
- blocks.push({
8356
- type: "table",
8357
- headers: table.headers,
8358
- rows: table.rows,
8359
- colAligns: table.colAligns
8360
- });
8361
- }
8362
- table = null;
8363
- };
8364
- const parseListItem = (text, indent) => {
8365
- const task = text.match(TASK_RE);
8366
- if (task) {
8367
- const done = task[1].toLowerCase() === "x";
8368
- return {
8369
- type: "paragraph",
8370
- text: `${done ? "\u2611" : "\u2610"} ${task[2]}`
8371
- };
8372
- }
8373
- return { type: "paragraph", text };
8374
- };
8375
8683
  for (let i = 0; i < lines.length; i++) {
8376
- const line = lines[i];
8684
+ const line = lines[i] ?? "";
8377
8685
  const trimmed = line.trim();
8378
8686
  if (code) {
8379
- const endMatch = trimmed.match(FENCE_RE);
8380
- if (endMatch && endMatch[1][0] === code.fence[0] && endMatch[1].length >= code.fence.length) {
8687
+ if (isClosingFence(line, code.fence)) {
8381
8688
  flushCode();
8382
8689
  continue;
8383
8690
  }
8384
8691
  code.lines.push(line);
8385
8692
  continue;
8386
8693
  }
8387
- const fenceMatch = trimmed.match(FENCE_RE);
8388
- if (fenceMatch) {
8694
+ const fence = parseOpeningFence(line);
8695
+ if (fence) {
8389
8696
  flushAll();
8390
- flushTable();
8391
- code = { fence: fenceMatch[1], lang: fenceMatch[2], lines: [] };
8697
+ code = { fence: fence.fence, lang: fence.lang, lines: [] };
8392
8698
  continue;
8393
8699
  }
8394
8700
  if (trimmed === "") {
8395
- flushTable();
8396
8701
  flushAll();
8397
8702
  continue;
8398
8703
  }
8399
- const hrMatch = trimmed.match(HR_RE);
8400
- if (hrMatch) {
8401
- flushTable();
8704
+ const heading = line.match(HEADING_RE);
8705
+ if (heading) {
8402
8706
  flushAll();
8403
- blocks.push({ type: "hr" });
8707
+ const text = (heading[2] ?? "").replace(/[ \t]+#+[ \t]*$/, "").trim();
8708
+ blocks.push({ type: "heading", level: heading[1].length, text });
8404
8709
  continue;
8405
8710
  }
8406
- const heading = trimmed.match(HEADING_RE);
8407
- if (heading) {
8408
- flushTable();
8711
+ const setext = lines[i + 1]?.match(SETEXT_RE);
8712
+ if (setext && isSetextHeadingText(line)) {
8713
+ flushAll();
8714
+ blocks.push({ type: "heading", level: setext[1][0] === "=" ? 1 : 2, text: trimmed });
8715
+ i += 1;
8716
+ continue;
8717
+ }
8718
+ if (isHorizontalRule(trimmed)) {
8409
8719
  flushAll();
8410
- blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() });
8720
+ blocks.push({ type: "hr" });
8411
8721
  continue;
8412
8722
  }
8413
8723
  const quoteMatch = trimmed.match(QUOTE_RE);
8414
8724
  if (quoteMatch) {
8415
- flushTable();
8416
8725
  flushParagraph();
8417
- flushList();
8418
8726
  flushCode();
8419
8727
  quote.push(quoteMatch[1]);
8420
8728
  continue;
8421
8729
  }
8422
- const indentMatch = line.match(INDENT_RE);
8423
- const indent = indentMatch?.[1].length ?? 0;
8424
- const taskMatch = trimmed.match(TASK_RE);
8425
- if (taskMatch) {
8426
- flushTable();
8730
+ const parsedList = parseListBlock(lines, i);
8731
+ if (parsedList) {
8427
8732
  flushParagraph();
8428
8733
  flushQuote();
8429
8734
  flushCode();
8430
- if (!list || list.ordered || indent !== list.indent) {
8431
- flushList();
8432
- list = { ordered: false, items: [], indent };
8433
- }
8434
- list.items.push(parseListItem(trimmed, indent));
8735
+ blocks.push(parsedList.block);
8736
+ i = parsedList.nextIndex - 1;
8435
8737
  continue;
8436
8738
  }
8437
- const ulMatch = trimmed.match(UL_RE);
8438
- if (ulMatch) {
8439
- flushTable();
8440
- flushParagraph();
8441
- flushQuote();
8442
- flushCode();
8443
- if (!list || list.ordered || indent !== list.indent) {
8444
- flushList();
8445
- list = { ordered: false, items: [], indent };
8446
- }
8447
- list.items.push(parseListItem(ulMatch[2], indent));
8739
+ const markdownTable = parseMarkdownTable(lines, i);
8740
+ if (markdownTable) {
8741
+ flushAll();
8742
+ blocks.push(markdownTable.block);
8743
+ i = markdownTable.nextIndex - 1;
8448
8744
  continue;
8449
8745
  }
8450
- const olMatch = trimmed.match(OL_RE);
8451
- if (olMatch) {
8452
- flushTable();
8453
- flushParagraph();
8454
- flushQuote();
8455
- flushCode();
8456
- if (!list || !list.ordered || indent !== list.indent) {
8457
- flushList();
8458
- list = { ordered: true, items: [], indent };
8459
- }
8460
- list.items.push(parseListItem(olMatch[2], indent));
8746
+ const asciiTable = parseAsciiTable(lines, i);
8747
+ if (asciiTable) {
8748
+ flushAll();
8749
+ blocks.push(asciiTable.block);
8750
+ i = asciiTable.nextIndex - 1;
8461
8751
  continue;
8462
8752
  }
8463
- const sepMatch = trimmed.match(/^\|?\s*(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?$/);
8464
- if (sepMatch) {
8465
- const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
8466
- if (cells.length >= 2 && cells.every((c) => /^:?-+:?$/.test(c))) {
8467
- if (table) {
8468
- table.colAligns = cells.map((c) => {
8469
- if (c.startsWith(":") && c.endsWith(":")) return "center";
8470
- if (c.endsWith(":")) return "right";
8471
- return "left";
8472
- });
8473
- continue;
8474
- }
8475
- }
8753
+ if (quote.length > 0) {
8754
+ quote.push(trimmed);
8755
+ continue;
8476
8756
  }
8477
- const rowMatch = trimmed.match(/^\|?.+\|.+\|?$/);
8478
- if (rowMatch) {
8479
- const cells = trimmed.split("|").map((c) => c.trim()).filter((c, idx, arr) => {
8480
- if (idx === 0 && c === "") return false;
8481
- if (idx === arr.length - 1 && c === "") return false;
8482
- return true;
8483
- });
8484
- const isWindowsPath = cells.length === 2 && (/^[A-Za-z]:\\/.test(cells[0]) || /^\\\\/.test(cells[0]));
8485
- if (cells.length >= 2 && cells.every((c) => c.length > 0) && !isWindowsPath) {
8486
- if (table) {
8487
- table.rows.push(cells);
8488
- } else {
8489
- flushAll();
8490
- table = { headers: cells, rows: [], colAligns: [] };
8757
+ flushQuote();
8758
+ flushCode();
8759
+ paragraph.push(paragraphLine(line));
8760
+ }
8761
+ flushAll();
8762
+ return blocks;
8763
+ }
8764
+ function paragraphLine(line) {
8765
+ const hardBreak = /(?: {2,}|\\)$/.test(line);
8766
+ const text = line.trim().replace(/\\$/, "");
8767
+ return hardBreak ? `${text}
8768
+ ` : text;
8769
+ }
8770
+ function joinParagraphLines(lines) {
8771
+ let result = "";
8772
+ for (const line of lines) {
8773
+ if (result && !result.endsWith("\n")) result += " ";
8774
+ result += line;
8775
+ }
8776
+ return result.trim();
8777
+ }
8778
+ function parseListMarker(line) {
8779
+ const match = line.match(/^(\s*)(?:(\d{1,9})[.)]|([-+*]))\s+(.*)$/);
8780
+ if (!match) return null;
8781
+ return {
8782
+ indent: match[1].replace(/\t/g, " ").length,
8783
+ ordered: Boolean(match[2]),
8784
+ start: match[2] ? Number.parseInt(match[2], 10) : 1,
8785
+ text: match[4]
8786
+ };
8787
+ }
8788
+ function parseListItem(text) {
8789
+ const task = text.match(TASK_CONTENT_RE);
8790
+ return {
8791
+ type: "paragraph",
8792
+ text: task ? `${task[1].toLowerCase() === "x" ? "\u2611" : "\u2610"} ${task[2]}` : text
8793
+ };
8794
+ }
8795
+ function parseListBlock(lines, start, depth = 0) {
8796
+ const first2 = parseListMarker(lines[start] ?? "");
8797
+ if (!first2) return null;
8798
+ const items = [];
8799
+ let nextIndex = start;
8800
+ while (nextIndex < lines.length) {
8801
+ const marker = parseListMarker(lines[nextIndex] ?? "");
8802
+ if (!marker || marker.indent !== first2.indent || marker.ordered !== first2.ordered) break;
8803
+ const item = parseListItem(marker.text);
8804
+ nextIndex += 1;
8805
+ while (nextIndex < lines.length) {
8806
+ if ((lines[nextIndex] ?? "").trim() === "") {
8807
+ let afterBlank = nextIndex;
8808
+ while (afterBlank < lines.length && (lines[afterBlank] ?? "").trim() === "") {
8809
+ afterBlank += 1;
8491
8810
  }
8492
- continue;
8493
- }
8494
- }
8495
- const asciiSepMatch = trimmed.match(/^[\s\-─┄┈━┅]{3,}$/);
8496
- if (asciiSepMatch) {
8497
- if (table && table.headers.length > 0 && table.colAligns.length === 0) {
8498
- table.colAligns = table.headers.map(() => "left");
8499
- continue;
8500
- }
8501
- if (paragraph.length === 1) {
8502
- const prevLine = paragraph[0].trim();
8503
- const tokens3 = prevLine.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
8504
- if (tokens3.length >= 2 && tokens3.length <= 10) {
8505
- paragraph = [];
8506
- flushList();
8507
- flushQuote();
8508
- flushCode();
8509
- table = { headers: tokens3, rows: [], colAligns: tokens3.map(() => "left") };
8510
- continue;
8811
+ const followingMarker2 = parseListMarker(lines[afterBlank] ?? "");
8812
+ if (!followingMarker2 || followingMarker2.indent < first2.indent) {
8813
+ nextIndex = afterBlank;
8814
+ break;
8511
8815
  }
8816
+ nextIndex = afterBlank;
8817
+ if (followingMarker2.indent === first2.indent) break;
8512
8818
  }
8513
- }
8514
- if (table && table.headers.length > 0 && !trimmed.includes("|")) {
8515
- const tokens3 = trimmed.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
8516
- if (tokens3.length >= 2 && tokens3.length <= table.headers.length + 2) {
8517
- const looksLikeCode = tokens3.some((t) => t.startsWith("//") || t.startsWith("#") || t.startsWith("/*"));
8518
- const looksLikePath = tokens3.length === 2 && (/^[A-Za-z]:\\/.test(tokens3[0]) || /^\\\\/.test(tokens3[0]));
8519
- if (!looksLikeCode && !looksLikePath) {
8520
- table.rows.push(tokens3);
8819
+ const followingMarker = parseListMarker(lines[nextIndex] ?? "");
8820
+ if (followingMarker) {
8821
+ if (followingMarker.indent <= first2.indent) break;
8822
+ if (depth >= MAX_LIST_DEPTH) {
8823
+ item.text = `${item.text ?? ""} ${followingMarker.text}`.trim();
8824
+ nextIndex += 1;
8521
8825
  continue;
8522
8826
  }
8827
+ const nested = parseListBlock(lines, nextIndex, depth + 1);
8828
+ if (!nested) break;
8829
+ item.children ??= [];
8830
+ item.children.push(nested.block);
8831
+ nextIndex = nested.nextIndex;
8832
+ continue;
8523
8833
  }
8524
- }
8525
- flushTable();
8526
- flushList();
8527
- flushQuote();
8528
- flushCode();
8529
- paragraph.push(trimmed);
8834
+ if (interruptsList(lines, nextIndex)) break;
8835
+ const continuation = (lines[nextIndex] ?? "").trim();
8836
+ item.text = `${item.text ?? ""} ${continuation}`.trim();
8837
+ nextIndex += 1;
8838
+ }
8839
+ items.push(item);
8840
+ }
8841
+ const block = { type: first2.ordered ? "ol" : "ul", items };
8842
+ if (first2.ordered && first2.start !== 1) block.start = first2.start;
8843
+ return { block, nextIndex };
8844
+ }
8845
+ function interruptsList(lines, index) {
8846
+ const line = lines[index] ?? "";
8847
+ const trimmed = line.trim();
8848
+ return Boolean(
8849
+ line.match(HEADING_RE) || trimmed.match(QUOTE_RE) || parseOpeningFence(line) || isHorizontalRule(trimmed) || parseMarkdownTable(lines, index) || parseAsciiTable(lines, index)
8850
+ );
8851
+ }
8852
+ function parseOpeningFence(line) {
8853
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
8854
+ if (!match) return null;
8855
+ const info = match[2].trim();
8856
+ if (match[1][0] === "`" && info.includes("`")) return null;
8857
+ return { fence: match[1], lang: info.split(/\s+/, 1)[0] ?? "" };
8858
+ }
8859
+ function isClosingFence(line, opening) {
8860
+ const match = line.match(/^ {0,3}(`+|~+)[ \t]*$/);
8861
+ return Boolean(
8862
+ match && match[1][0] === opening[0] && match[1].length >= opening.length
8863
+ );
8864
+ }
8865
+ function isHorizontalRule(line) {
8866
+ return /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/.test(line);
8867
+ }
8868
+ function isSetextHeadingText(line) {
8869
+ const trimmed = line.trim();
8870
+ return trimmed !== "" && !HEADING_RE.test(line) && !QUOTE_RE.test(trimmed) && !UL_RE.test(trimmed) && !OL_RE.test(trimmed) && !parseOpeningFence(line);
8871
+ }
8872
+ function parseMarkdownTable(lines, start) {
8873
+ const headers = splitTableRow(lines[start] ?? "");
8874
+ const delimiter = splitTableRow(lines[start + 1] ?? "");
8875
+ if (!headers || !delimiter || headers.length < 2 || headers.length > MAX_TABLE_COLUMNS || delimiter.length !== headers.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
8876
+ return null;
8530
8877
  }
8531
- flushTable();
8532
- flushAll();
8533
- return blocks;
8878
+ const colAligns = delimiter.map((cell) => {
8879
+ if (cell.startsWith(":") && cell.endsWith(":")) return "center";
8880
+ if (cell.endsWith(":")) return "right";
8881
+ return "left";
8882
+ });
8883
+ const rows = [];
8884
+ let nextIndex = start + 2;
8885
+ while (nextIndex < lines.length) {
8886
+ const cells = splitTableRow(lines[nextIndex] ?? "");
8887
+ if (!cells) break;
8888
+ rows.push(headers.map((_, index) => cells[index] ?? ""));
8889
+ nextIndex += 1;
8890
+ }
8891
+ return {
8892
+ block: { type: "table", headers, rows, colAligns },
8893
+ nextIndex
8894
+ };
8895
+ }
8896
+ function splitTableRow(line) {
8897
+ const source = line.trim();
8898
+ if (!source.includes("|")) return null;
8899
+ const cells = [];
8900
+ let cell = "";
8901
+ let codeFenceLength = 0;
8902
+ let foundSeparator = false;
8903
+ for (let index = 0; index < source.length; index += 1) {
8904
+ const character = source[index] ?? "";
8905
+ if (character === "\\" && source[index + 1] === "|") {
8906
+ cell += "|";
8907
+ index += 1;
8908
+ continue;
8909
+ }
8910
+ if (character === "`") {
8911
+ let end = index + 1;
8912
+ while (source[end] === "`") end += 1;
8913
+ const runLength = end - index;
8914
+ if (codeFenceLength === 0) codeFenceLength = runLength;
8915
+ else if (codeFenceLength === runLength) codeFenceLength = 0;
8916
+ cell += source.slice(index, end);
8917
+ index = end - 1;
8918
+ continue;
8919
+ }
8920
+ if (character === "|" && codeFenceLength === 0) {
8921
+ foundSeparator = true;
8922
+ cells.push(cell.trim());
8923
+ cell = "";
8924
+ continue;
8925
+ }
8926
+ cell += character;
8927
+ }
8928
+ cells.push(cell.trim());
8929
+ if (!foundSeparator) return null;
8930
+ if (cells[0] === "") cells.shift();
8931
+ if (cells[cells.length - 1] === "") cells.pop();
8932
+ return cells.length >= 2 ? cells : null;
8933
+ }
8934
+ function parseAsciiTable(lines, start) {
8935
+ const headers = splitAsciiCells(lines[start] ?? "");
8936
+ if (headers.length < 2 || headers.length > MAX_TABLE_COLUMNS) return null;
8937
+ const separator = (lines[start + 1] ?? "").trim();
8938
+ const separatorCells = splitAsciiCells(separator);
8939
+ const validSeparator = /^[─┄┈━┅-]{3,}$/.test(separator) || separatorCells.length === headers.length && separatorCells.every((cell) => /^[─┄┈━┅-]{3,}$/.test(cell));
8940
+ if (!validSeparator) return null;
8941
+ const rows = [];
8942
+ let nextIndex = start + 2;
8943
+ while (nextIndex < lines.length) {
8944
+ const cells = splitAsciiCells(lines[nextIndex] ?? "");
8945
+ if (cells.length !== headers.length) break;
8946
+ rows.push(cells);
8947
+ nextIndex += 1;
8948
+ }
8949
+ if (rows.length === 0) return null;
8950
+ return {
8951
+ block: {
8952
+ type: "table",
8953
+ headers,
8954
+ rows,
8955
+ colAligns: headers.map(() => "left")
8956
+ },
8957
+ nextIndex
8958
+ };
8959
+ }
8960
+ function splitAsciiCells(line) {
8961
+ return line.trim().split(/\s{2,}/).map((cell) => cell.trim()).filter(Boolean);
8534
8962
  }
8535
8963
  var MAX_INLINE_DEPTH = 10;
8964
+ var INLINE_ESCAPE_BASE = 57344;
8965
+ var ESCAPABLE_MARKDOWN = ["\\", "`", "*", "_", "[", "]", "{", "}", "(", ")", "#", "+", "-", ".", "!", "|", ">"];
8966
+ function protectInlineEscapes(text) {
8967
+ return text.replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, (_match, character) => {
8968
+ const index = ESCAPABLE_MARKDOWN.indexOf(character);
8969
+ return String.fromCodePoint(INLINE_ESCAPE_BASE + index);
8970
+ });
8971
+ }
8972
+ function restoreInlineEscapes(text) {
8973
+ return [...text].map((character) => {
8974
+ const index = character.codePointAt(0) - INLINE_ESCAPE_BASE;
8975
+ return index >= 0 && index < ESCAPABLE_MARKDOWN.length ? ESCAPABLE_MARKDOWN[index] : character;
8976
+ }).join("");
8977
+ }
8536
8978
  function inline(text, depth = 0) {
8537
8979
  if (depth > MAX_INLINE_DEPTH) {
8538
- return text;
8980
+ return restoreInlineEscapes(text);
8539
8981
  }
8540
8982
  const nodes = [];
8541
- let rest = text;
8983
+ let rest = protectInlineEscapes(text);
8542
8984
  let key = 0;
8543
8985
  const matchers = [
8544
8986
  {
8545
- re: /(!\[)([^\]]*)\]\(([^)]+)\)/,
8987
+ re: /(!\[)([^\]]*)\]\(((?:[^()\\]|\\.|\([^()]*\))+?)\)/,
8546
8988
  handler: (m) => /* @__PURE__ */ jsxs10(Text10, { color: "cyan", bold: true, children: [
8547
8989
  "[img: ",
8548
- m[2],
8990
+ restoreInlineEscapes(m[2]),
8549
8991
  "]"
8550
8992
  ] }, key++)
8551
8993
  },
8552
8994
  {
8553
- re: /(\[)([^\]]*)\]\(([^)]+)\)/,
8554
- handler: (m) => /* @__PURE__ */ jsxs10(Fragment4, { children: [
8555
- /* @__PURE__ */ jsx11(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }, key++),
8556
- /* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
8557
- "(",
8558
- m[3],
8559
- ")"
8560
- ] }, key++)
8561
- ] })
8995
+ re: /(\[)([^\]]*)\]\(((?:[^()\\]|\\.|\([^()]*\))+?)\)/,
8996
+ handler: (m) => {
8997
+ const fragmentKey = key++;
8998
+ return /* @__PURE__ */ jsxs10(Fragment4, { children: [
8999
+ /* @__PURE__ */ jsx11(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }),
9000
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
9001
+ "(",
9002
+ restoreInlineEscapes(m[3]),
9003
+ ")"
9004
+ ] })
9005
+ ] }, fragmentKey);
9006
+ }
8562
9007
  },
8563
9008
  {
8564
9009
  re: /(`+)([^`]+?)\1/,
8565
- handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
9010
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: restoreInlineEscapes(m[2]) }, key++)
8566
9011
  },
8567
9012
  {
8568
- re: /(\*\*)(.+?)\1/,
9013
+ re: /(\*\*\*|___)(?=\S)(.*?\S)\1/,
9014
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, italic: true, children: inline(m[2], depth + 1) }, key++)
9015
+ },
9016
+ {
9017
+ re: /(\*\*)(?=\S)(.*?\S)\1/,
8569
9018
  handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
8570
9019
  },
8571
9020
  {
8572
- re: /(__)(.+?)\1/,
9021
+ re: /(__)(?=\S)(.*?\S)\1/,
8573
9022
  handler: (m) => /* @__PURE__ */ jsx11(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
8574
9023
  },
8575
9024
  {
8576
- re: /(~~)(.+?)\1/,
8577
- handler: (m) => /* @__PURE__ */ jsx11(Text10, { strikethrough: true, children: m[2] }, key++)
9025
+ re: /(~~)(?=\S)(.*?\S)\1/,
9026
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { strikethrough: true, children: inline(m[2], depth + 1) }, key++)
9027
+ },
9028
+ {
9029
+ re: /(\*)(?=\S)(.*?\S)\1/,
9030
+ handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
8578
9031
  },
8579
9032
  {
8580
- re: /(\*)(.+?)\1/,
9033
+ re: /(?<!\w)(_)(?=\S)(.*?\S)\1(?!\w)/,
8581
9034
  handler: (m) => /* @__PURE__ */ jsx11(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
8582
9035
  }
8583
9036
  ];
@@ -8590,11 +9043,11 @@ function inline(text, depth = 0) {
8590
9043
  }
8591
9044
  }
8592
9045
  if (!best) {
8593
- nodes.push(rest);
9046
+ nodes.push(restoreInlineEscapes(rest));
8594
9047
  break;
8595
9048
  }
8596
9049
  const idx = best.match.index ?? 0;
8597
- if (idx > 0) nodes.push(rest.slice(0, idx));
9050
+ if (idx > 0) nodes.push(restoreInlineEscapes(rest.slice(0, idx)));
8598
9051
  nodes.push(best.handler(best.match));
8599
9052
  rest = rest.slice(idx + best.match[0].length);
8600
9053
  }
@@ -8604,12 +9057,19 @@ function inline(text, depth = 0) {
8604
9057
  // src/ui/components/Transcript.tsx
8605
9058
  import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
8606
9059
  var HEADER = { kind: "header" };
9060
+ function assistantThinkingForFrame(bubble, frozenThinking) {
9061
+ return bubble.streaming && bubble.text !== "" ? frozenThinking ?? bubble.thinking : bubble.thinking;
9062
+ }
8607
9063
  function firstMutableBubbleIndex(bubbles) {
8608
9064
  return bubbles.findIndex(
8609
9065
  (bubble) => bubble.kind === "assistant" && bubble.streaming || bubble.kind === "tool" && bubble.state === "running" || bubble.kind === "subagent" && bubble.state === "running"
8610
9066
  );
8611
9067
  }
8612
- var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
9068
+ var Transcript = memo2(function Transcript2({
9069
+ bubbles,
9070
+ workspace,
9071
+ maxLiveRows = 12
9072
+ }) {
8613
9073
  const liveAt = firstMutableBubbleIndex(bubbles);
8614
9074
  const stableCount = liveAt === -1 ? bubbles.length : liveAt;
8615
9075
  const stableRef = useRef3([]);
@@ -8623,21 +9083,36 @@ var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
8623
9083
  }
8624
9084
  const live = liveAt === -1 ? [] : bubbles.slice(liveAt);
8625
9085
  const staticItems = [HEADER, ...stableRef.current];
8626
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginBottom: 1, flexShrink: 1, overflowY: "hidden", children: [
8627
- /* @__PURE__ */ jsx12(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx12(Logo, { workspace }, "kitcode-header") : /* @__PURE__ */ jsx12(BubbleView, { bubble: item }, item.id) }),
8628
- /* @__PURE__ */ jsx12(
8629
- Box12,
8630
- {
8631
- flexDirection: "column",
8632
- flexShrink: 1,
8633
- overflowY: "hidden",
8634
- justifyContent: "flex-end",
8635
- children: live.map((bubble) => /* @__PURE__ */ jsx12(BubbleView, { bubble }, bubble.id))
8636
- }
8637
- )
8638
- ] });
9086
+ return /* @__PURE__ */ jsxs11(
9087
+ Box12,
9088
+ {
9089
+ flexDirection: "column",
9090
+ marginBottom: 1,
9091
+ flexShrink: 1,
9092
+ minHeight: 0,
9093
+ overflowY: "hidden",
9094
+ children: [
9095
+ /* @__PURE__ */ jsx12(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx12(Logo, { workspace }, "kitcode-header") : /* @__PURE__ */ jsx12(BubbleView, { bubble: item }, item.id) }),
9096
+ /* @__PURE__ */ jsx12(
9097
+ Box12,
9098
+ {
9099
+ flexDirection: "column",
9100
+ flexShrink: 1,
9101
+ minHeight: 0,
9102
+ maxHeight: maxLiveRows,
9103
+ overflowY: "hidden",
9104
+ justifyContent: "flex-end",
9105
+ children: live.map((bubble) => /* @__PURE__ */ jsx12(BubbleView, { bubble, maxRows: maxLiveRows }, bubble.id))
9106
+ }
9107
+ )
9108
+ ]
9109
+ }
9110
+ );
8639
9111
  });
8640
- var BubbleView = memo2(function BubbleView2({ bubble }) {
9112
+ var BubbleView = memo2(function BubbleView2({
9113
+ bubble,
9114
+ maxRows
9115
+ }) {
8641
9116
  const theme = useTheme();
8642
9117
  if (bubble.kind === "user") {
8643
9118
  return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: theme.accent, bold: true, children: [
@@ -8650,20 +9125,62 @@ var BubbleView = memo2(function BubbleView2({ bubble }) {
8650
9125
  return /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color, children: bubble.text }) });
8651
9126
  }
8652
9127
  if (bubble.kind === "assistant") {
8653
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
8654
- bubble.thinking.trim() !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: bubble.thinking.trim() }),
8655
- bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: bubble.text }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
8656
- bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
8657
- /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
8658
- " thinking"
8659
- ] })
8660
- ] });
9128
+ return /* @__PURE__ */ jsx12(AssistantView, { bubble, maxRows });
8661
9129
  }
8662
9130
  if (bubble.kind === "subagent") {
8663
9131
  return /* @__PURE__ */ jsx12(SubagentView, { bubble });
8664
9132
  }
8665
9133
  return /* @__PURE__ */ jsx12(ToolView, { bubble });
8666
9134
  });
9135
+ function AssistantView({ bubble, maxRows }) {
9136
+ const { columns } = useWindowSize3();
9137
+ const frozenThinking = useRef3(void 0);
9138
+ const answering = bubble.streaming && bubble.text !== "";
9139
+ if (!answering) frozenThinking.current = void 0;
9140
+ if (answering && frozenThinking.current === void 0) {
9141
+ frozenThinking.current = bubble.thinking;
9142
+ }
9143
+ const visibleThinking = assistantThinkingForFrame(bubble, frozenThinking.current);
9144
+ const liveBudget = bubble.streaming && maxRows !== void 0 ? Math.max(1, maxRows - 1) : void 0;
9145
+ const thinkingBudget = liveBudget === void 0 ? void 0 : bubble.text !== "" ? Math.min(3, Math.max(0, liveBudget - 1)) : Math.max(0, liveBudget - 1);
9146
+ const frameThinking = thinkingBudget === void 0 ? visibleThinking.trim() : clipTextToRows(visibleThinking.trim(), thinkingBudget, columns);
9147
+ const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9148
+ const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9149
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9150
+ frameThinking !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: frameThinking }),
9151
+ bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9152
+ bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9153
+ /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
9154
+ " thinking"
9155
+ ] })
9156
+ ] });
9157
+ }
9158
+ function clipTextToRows(text, maxRows, columns) {
9159
+ if (text === "" || maxRows <= 0) return "";
9160
+ const width = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 80;
9161
+ const rows = text.replace(/\r\n?/g, "\n").split("\n").flatMap((line) => wrapVisualLine(line, width));
9162
+ if (rows.length <= maxRows) return rows.join("\n");
9163
+ const tail2 = rows.slice(-Math.max(1, maxRows - 1));
9164
+ return maxRows === 1 ? tail2.at(-1) ?? "" : ["\u2026", ...tail2].join("\n");
9165
+ }
9166
+ function wrapVisualLine(line, columns) {
9167
+ if (line === "") return [""];
9168
+ const rows = [];
9169
+ let current = "";
9170
+ let currentWidth = 0;
9171
+ for (const character of line) {
9172
+ const width = Math.max(1, stringWidth2(character));
9173
+ if (current !== "" && currentWidth + width > columns) {
9174
+ rows.push(current);
9175
+ current = "";
9176
+ currentWidth = 0;
9177
+ }
9178
+ current += character;
9179
+ currentWidth += width;
9180
+ }
9181
+ if (current !== "" || rows.length === 0) rows.push(current);
9182
+ return rows;
9183
+ }
8667
9184
  function ToolView({ bubble }) {
8668
9185
  const theme = useTheme();
8669
9186
  const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
@@ -8971,7 +9488,7 @@ function App({
8971
9488
  warnings = []
8972
9489
  }) {
8973
9490
  const { exit } = useApp();
8974
- const { rows } = useWindowSize2();
9491
+ const { rows } = useWindowSize4();
8975
9492
  const [transcript, setTranscript] = useState5(
8976
9493
  () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
8977
9494
  );
@@ -9064,11 +9581,10 @@ function App({
9064
9581
  );
9065
9582
  useEffect2(() => {
9066
9583
  const check = runtime.startupUpdateCheck();
9067
- if (!check) return;
9068
9584
  let active2 = true;
9069
9585
  void check.then((result) => {
9070
9586
  if (active2 && result.status === "available") {
9071
- notice("info", strings.updateAvailable(result.latest.slice(0, 12), result.url));
9587
+ notice("info", strings.updateAvailable(result.latest, result.url));
9072
9588
  }
9073
9589
  });
9074
9590
  return () => {
@@ -9364,6 +9880,17 @@ function App({
9364
9880
  case "config":
9365
9881
  notice("info", strings.configAt(runtime.configPath()));
9366
9882
  return;
9883
+ case "update": {
9884
+ const result = await runtime.checkForUpdates();
9885
+ if (result.status === "available") {
9886
+ notice("info", strings.updateAvailable(result.latest, result.url));
9887
+ } else if (result.status === "current") {
9888
+ notice("info", strings.updateCurrent(result.current));
9889
+ } else {
9890
+ notice("warn", strings.updateFailed(result.reason));
9891
+ }
9892
+ return;
9893
+ }
9367
9894
  case "login":
9368
9895
  setSetup(true);
9369
9896
  return;
@@ -10091,6 +10618,10 @@ Rename the file if you want a different name.`);
10091
10618
  return;
10092
10619
  }
10093
10620
  if (!key.escape) return;
10621
+ if (input !== "") {
10622
+ setInput("");
10623
+ return;
10624
+ }
10094
10625
  if (busy && abort.current) {
10095
10626
  abort.current.abort();
10096
10627
  queueRef.current = [];
@@ -10151,7 +10682,8 @@ ${strings.configAt(runtime.configPath())}`);
10151
10682
  Transcript,
10152
10683
  {
10153
10684
  bubbles: transcript.bubbles,
10154
- workspace: runtime.cwd
10685
+ workspace: runtime.cwd,
10686
+ maxLiveRows: liveTranscriptRows(rows)
10155
10687
  },
10156
10688
  transcriptRevision
10157
10689
  ),
@@ -10349,6 +10881,12 @@ import { jsx as jsx14 } from "react/jsx-runtime";
10349
10881
  function clearTerminal() {
10350
10882
  if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
10351
10883
  }
10884
+ var TUI_RENDER_OPTIONS = {
10885
+ // Keep unchanged streamed lines in place instead of clearing and redrawing
10886
+ // the whole live region on every frame.
10887
+ incrementalRendering: true,
10888
+ maxFps: 30
10889
+ };
10352
10890
  async function startTui(options) {
10353
10891
  const { runtime, history, warnings, shutdown } = await boot({
10354
10892
  cwd: options.cwd ?? process.cwd(),
@@ -10358,10 +10896,10 @@ async function startTui(options) {
10358
10896
  mode: options.mode
10359
10897
  });
10360
10898
  clearTerminal();
10361
- const instance = render(/* @__PURE__ */ jsx14(App, { runtime, initialHistory: history, warnings }), {
10362
- incrementalRendering: true,
10363
- maxFps: 30
10364
- });
10899
+ const instance = render(
10900
+ /* @__PURE__ */ jsx14(App, { runtime, initialHistory: history, warnings }),
10901
+ TUI_RENDER_OPTIONS
10902
+ );
10365
10903
  try {
10366
10904
  await instance.waitUntilExit();
10367
10905
  } finally {