@kernelonpanic/kitcode 1.0.1-beta → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ var diagnosticsSchema = z.object({
62
62
  commands: z.array(z.string().trim().min(1).max(2e4)).max(8).default([])
63
63
  });
64
64
  var updatesSchema = z.object({
65
- checkOnStart: z.boolean().default(false)
65
+ checkOnStart: z.boolean().default(true)
66
66
  });
67
67
  var configSchema = z.object({
68
68
  version: z.literal(1).default(1),
@@ -658,6 +658,7 @@ import path3 from "path";
658
658
  import { randomUUID } from "crypto";
659
659
  import { readFile, realpath, rename, rm, writeFile } from "fs/promises";
660
660
  import path2 from "path";
661
+ var trustLock = Promise.resolve();
661
662
  var emptyTrust = () => ({ version: 1, workspaces: [] });
662
663
  async function canonicalWorkspace(cwd) {
663
664
  const resolved = await workspaceRootFor(cwd);
@@ -686,6 +687,7 @@ async function revokeWorkspaceTrust(cwd) {
686
687
  return workspace;
687
688
  }
688
689
  async function loadTrust() {
690
+ await trustLock;
689
691
  let parsed;
690
692
  try {
691
693
  parsed = JSON.parse(await readFile(trustPath, "utf8"));
@@ -702,15 +704,25 @@ async function loadTrust() {
702
704
  };
703
705
  }
704
706
  async function saveTrust(value) {
705
- await ensureDir(path2.dirname(trustPath));
706
- const temp = `${trustPath}.${randomUUID()}.tmp`;
707
+ const release = trustLock;
708
+ let resolveLock;
709
+ trustLock = new Promise((resolve3) => {
710
+ resolveLock = resolve3;
711
+ });
712
+ await release;
707
713
  try {
708
- await writeFile(temp, `${JSON.stringify(value, null, 2)}
714
+ await ensureDir(path2.dirname(trustPath));
715
+ const temp = `${trustPath}.${randomUUID()}.tmp`;
716
+ try {
717
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}
709
718
  `, { encoding: "utf8", mode: 384 });
710
- await rename(temp, trustPath);
711
- } catch (error) {
712
- await rm(temp, { force: true });
713
- throw error;
719
+ await rename(temp, trustPath);
720
+ } catch (error) {
721
+ await rm(temp, { force: true });
722
+ throw error;
723
+ }
724
+ } finally {
725
+ resolveLock();
714
726
  }
715
727
  }
716
728
 
@@ -1631,11 +1643,12 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1631
1643
  };
1632
1644
  }
1633
1645
  const pricing = resolvePricing(request.modelRef);
1634
- if (pricing && current.costUsd !== null) {
1646
+ if (current.costUsd !== null) {
1635
1647
  const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
1636
- const inputUsd = estimatedInput * pricing.input / 1e6;
1648
+ const effectivePricing = pricing ?? { input: 3e-3, output: 0.015 };
1649
+ const inputUsd = estimatedInput * effectivePricing.input / 1e6;
1637
1650
  const affordableOutput = Math.floor(
1638
- (remainingUsd - inputUsd) * 1e6 / pricing.output
1651
+ (remainingUsd - inputUsd) * 1e6 / effectivePricing.output
1639
1652
  );
1640
1653
  outputLimit = Math.min(outputLimit, affordableOutput);
1641
1654
  if (outputLimit < 1) {
@@ -1813,7 +1826,7 @@ function beginCheckpoint(options) {
1813
1826
  lastCheckpointTimestamp = Math.max(Date.now(), lastCheckpointTimestamp + 1);
1814
1827
  const id = `${String(lastCheckpointTimestamp).padStart(13, "0")}-${randomBytes2(4).toString("hex")}`;
1815
1828
  await writeCheckpoint(path5.join(sessionDir, `${id}.json`), stored);
1816
- await pruneCheckpoints(sessionDir);
1829
+ pruneCheckpoints(sessionDir).catch(() => void 0);
1817
1830
  return { id, paths: entries.map((entry) => entry.path) };
1818
1831
  }
1819
1832
  };
@@ -2315,7 +2328,7 @@ async function isFile2(file) {
2315
2328
 
2316
2329
  // src/core/attachments.ts
2317
2330
  import { execFile } from "child_process";
2318
- import { readFile as readFile6, stat as stat5 } from "fs/promises";
2331
+ import { lstat as lstat2, readFile as readFile6, stat as stat5 } from "fs/promises";
2319
2332
  import os2 from "os";
2320
2333
  import path7 from "path";
2321
2334
  import { fileURLToPath } from "url";
@@ -2328,6 +2341,8 @@ var SENSITIVE_AUTO_NAMES = /* @__PURE__ */ new Set([
2328
2341
  ".netrc",
2329
2342
  ".npmrc",
2330
2343
  ".pypirc",
2344
+ ".pgpass",
2345
+ ".my.cnf",
2331
2346
  "auth.json",
2332
2347
  "credentials.json",
2333
2348
  "id_dsa",
@@ -2337,11 +2352,17 @@ var SENSITIVE_AUTO_NAMES = /* @__PURE__ */ new Set([
2337
2352
  "kitcode.json",
2338
2353
  "secret.json",
2339
2354
  "secrets.json",
2340
- "tokens.json"
2355
+ "tokens.json",
2356
+ "kubeconfig",
2357
+ "service-account.json"
2341
2358
  ]);
2342
- var SENSITIVE_AUTO_EXTENSIONS = /* @__PURE__ */ new Set([".jks", ".key", ".keystore", ".p12", ".pem", ".pfx"]);
2359
+ var SENSITIVE_AUTO_EXTENSIONS = /* @__PURE__ */ new Set([".jks", ".key", ".keystore", ".p12", ".pem", ".pfx", ".p8", ".der"]);
2343
2360
  async function loadAttachment(cwd, requestedPath) {
2344
2361
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2362
+ const linkInfo = await lstat2(resolved).catch(() => null);
2363
+ if (linkInfo?.isSymbolicLink()) {
2364
+ throw new Error(`Cannot attach symbolic links: ${path7.basename(resolved)}`);
2365
+ }
2345
2366
  const info = await stat5(resolved).catch((error) => {
2346
2367
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
2347
2368
  throw error;
@@ -2352,16 +2373,23 @@ async function loadAttachment(cwd, requestedPath) {
2352
2373
  async function loadAutomaticAttachment(cwd, requestedPath) {
2353
2374
  if (!looksLikeAttachmentPath(requestedPath)) return null;
2354
2375
  const resolved = resolveAttachmentPath(cwd, requestedPath);
2355
- const info = await stat5(resolved).catch((error) => {
2356
- if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
2357
- throw error;
2358
- });
2359
- if (!info?.isFile()) return null;
2360
2376
  if (isSensitiveAutomaticPath(resolved)) {
2361
2377
  throw new Error(
2362
2378
  `For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
2363
2379
  );
2364
2380
  }
2381
+ if (path7.isAbsolute(resolved)) {
2382
+ const normalized = resolved.toLowerCase();
2383
+ const isProjectFile = normalized.includes(path7.sep + "src" + path7.sep) || normalized.includes(path7.sep + "lib" + path7.sep) || normalized.includes(path7.sep + "app" + path7.sep) || normalized.includes(path7.sep + "test" + path7.sep) || normalized.includes(path7.sep + "tests" + path7.sep) || normalized.includes(path7.sep + "public" + path7.sep) || normalized.includes(path7.sep + "assets" + path7.sep) || normalized.includes(path7.sep + "static" + path7.sep) || normalized.endsWith(".ts") || normalized.endsWith(".js") || normalized.endsWith(".tsx") || normalized.endsWith(".jsx") || normalized.endsWith(".py") || normalized.endsWith(".go") || normalized.endsWith(".rs") || normalized.endsWith(".java") || normalized.endsWith(".c") || normalized.endsWith(".cpp") || normalized.endsWith(".h") || normalized.endsWith(".hpp") || normalized.endsWith(".css") || normalized.endsWith(".html") || normalized.endsWith(".json") || normalized.endsWith(".yaml") || normalized.endsWith(".yml") || normalized.endsWith(".md") || normalized.endsWith(".txt") || normalized.endsWith(".toml") || normalized.endsWith(".ini") || normalized.endsWith(".cfg") || normalized.endsWith(".sh") || normalized.endsWith(".bat") || normalized.endsWith(".cmd") || normalized.endsWith(".ps1") || normalized.endsWith(".dockerfile") || normalized.endsWith(".makefile");
2384
+ if (!isProjectFile) return null;
2385
+ }
2386
+ const linkInfo = await lstat2(resolved).catch(() => null);
2387
+ if (linkInfo?.isSymbolicLink()) return null;
2388
+ const info = await stat5(resolved).catch((error) => {
2389
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
2390
+ throw error;
2391
+ });
2392
+ if (!info?.isFile()) return null;
2365
2393
  return loadResolvedAttachment(resolved, info.size);
2366
2394
  }
2367
2395
  function looksLikeAttachmentPath(value) {
@@ -2622,7 +2650,7 @@ function normalizeInputPath(value) {
2622
2650
  return value.slice(1, -1);
2623
2651
  }
2624
2652
  }
2625
- return value.replace(/\\([\\ "'()\[\]{}&;#$!])/g, (_match, character) => character);
2653
+ return value.replace(/\\([\\ "'()\[\]&;#$!])/g, (_match, character) => character);
2626
2654
  }
2627
2655
  function safeName(value) {
2628
2656
  return value.replace(/[\r\n\0-\x1f\x7f]/g, "").slice(0, 200) || "attachment";
@@ -2798,6 +2826,8 @@ var STEP_LIMIT_NOTE = `The subagent was stopped after ${MAX_SUBAGENT_STEPS} mode
2798
2826
  var SUBAGENT_SYSTEM = [
2799
2827
  "You are a subagent launched by the main kitcode agent to carry out one task on your own.",
2800
2828
  "",
2829
+ "SECURITY CRITICAL: You must NEVER execute commands that read, modify, or exfiltrate sensitive files including but not limited to: /etc/passwd, /etc/shadow, ~/.ssh/*, ~/.aws/*, ~/.env*, .git/config, credentials files, or any files outside the workspace. Never run `curl ... | bash`, `wget ... | bash`, or similar patterns. Never send data to external servers.",
2830
+ "",
2801
2831
  "Nobody is reading along and nobody can answer you. Never ask a question, never stop to propose a plan, never wait for approval. If the request is ambiguous, take the most reasonable reading, do the work, and state the assumption in your answer.",
2802
2832
  "",
2803
2833
  'Your final message is the return value \u2014 it is the only thing the caller ever sees. End with the answer itself: the findings, the file paths, the conclusion. Never end with "I will now..." or a recap of the steps you took. Carry over every concrete detail the caller needs (paths, names, line numbers) and leave out the narration of how you found them.'
@@ -2971,10 +3001,14 @@ function printableInput(input) {
2971
3001
  }
2972
3002
  async function invoke(call, tool, input, signal) {
2973
3003
  try {
2974
- const result = await call(tool, input, signal);
3004
+ if (input !== null && typeof input !== "object" || Array.isArray(input)) {
3005
+ return { content: `MCP tool "${tool}" received invalid input: arguments must be a JSON object.`, isError: true };
3006
+ }
3007
+ const result = await call(tool, input ?? {}, signal);
2975
3008
  return { content: flattenResult(result), isError: result.isError };
2976
3009
  } catch (error) {
2977
- return { content: error instanceof Error ? error.message : String(error), isError: true };
3010
+ const message = error instanceof Error ? error.message : String(error);
3011
+ return { content: redactSecrets(message), isError: true };
2978
3012
  }
2979
3013
  }
2980
3014
  function flattenResult(result) {
@@ -3016,10 +3050,18 @@ function clip(value) {
3016
3050
  // package.json
3017
3051
  var package_default = {
3018
3052
  name: "@kernelonpanic/kitcode",
3019
- version: "1.0.1-beta",
3053
+ version: "1.1.1",
3020
3054
  description: "Terminal coding agent with a config you never have to write by hand",
3021
3055
  type: "module",
3022
3056
  license: "MIT",
3057
+ homepage: "https://github.com/KernelEditor/KitCode#readme",
3058
+ bugs: {
3059
+ url: "https://github.com/KernelEditor/KitCode/issues"
3060
+ },
3061
+ repository: {
3062
+ type: "git",
3063
+ url: "git+https://github.com/KernelEditor/KitCode.git"
3064
+ },
3023
3065
  bin: {
3024
3066
  kitcode: "dist/index.js"
3025
3067
  },
@@ -3075,8 +3117,8 @@ var package_default = {
3075
3117
 
3076
3118
  // src/version.ts
3077
3119
  var KITCODE_VERSION = package_default.version;
3078
- var KITCODE_REPOSITORY = "PanicOnKernel/KitCode";
3079
- var KITCODE_COMMIT = true ? "bf1fc071e8deae678110781210a0b8d4544d21f9" : "development";
3120
+ var KITCODE_REPOSITORY = "KernelEditor/KitCode";
3121
+ var KITCODE_COMMIT = true ? "a57e54102a5e1ca1e60cec4ee9c629c890d27057" : "development";
3080
3122
 
3081
3123
  // src/mcp/client.ts
3082
3124
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3100,6 +3142,36 @@ var inheritedEnvKeys = /* @__PURE__ */ new Set([
3100
3142
  "APPDATA",
3101
3143
  "LOCALAPPDATA"
3102
3144
  ]);
3145
+ var blockedEnvKeys = /* @__PURE__ */ new Set([
3146
+ "ANTHROPIC_API_KEY",
3147
+ "OPENAI_API_KEY",
3148
+ "AWS_ACCESS_KEY_ID",
3149
+ "AWS_SECRET_ACCESS_KEY",
3150
+ "AWS_SESSION_TOKEN",
3151
+ "GITHUB_TOKEN",
3152
+ "GH_TOKEN",
3153
+ "GIT_TOKEN",
3154
+ "NPM_TOKEN",
3155
+ "PYPI_TOKEN",
3156
+ "TWINE_PASSWORD",
3157
+ "DOCKER_PASSWORD",
3158
+ "DATABASE_URL",
3159
+ "REDIS_URL",
3160
+ "MONGODB_URI",
3161
+ "POSTGRES_PASSWORD",
3162
+ "MYSQL_PASSWORD",
3163
+ "PRIVATE_KEY",
3164
+ "SSH_KEY",
3165
+ "GPG_KEY",
3166
+ "SIGNING_KEY",
3167
+ "COOKIE",
3168
+ "SESSION_SECRET",
3169
+ "JWT_SECRET",
3170
+ "ENCRYPTION_KEY",
3171
+ "MASTER_KEY",
3172
+ "API_KEY",
3173
+ "API_SECRET"
3174
+ ]);
3103
3175
  function createMcpManager(servers) {
3104
3176
  const serverStates = /* @__PURE__ */ new Map();
3105
3177
  const sessions = /* @__PURE__ */ new Map();
@@ -3246,7 +3318,9 @@ function configuredSecrets(config) {
3246
3318
  function inheritedEnv(source = process.env) {
3247
3319
  const env = {};
3248
3320
  for (const [key, value] of Object.entries(source)) {
3249
- if (value !== void 0 && inheritedEnvKeys.has(key)) env[key] = value;
3321
+ if (value !== void 0 && inheritedEnvKeys.has(key) && !blockedEnvKeys.has(key)) {
3322
+ env[key] = value;
3323
+ }
3250
3324
  }
3251
3325
  return env;
3252
3326
  }
@@ -4071,7 +4145,7 @@ function isFileEdit(tool) {
4071
4145
  }
4072
4146
 
4073
4147
  // src/tools/edit.ts
4074
- import { readFile as readFile8, stat as stat6, writeFile as writeFile6 } from "fs/promises";
4148
+ import { lstat as lstat3, readFile as readFile8, stat as stat6, writeFile as writeFile6 } from "fs/promises";
4075
4149
  var MAX_FILE_BYTES2 = 5e6;
4076
4150
  function countOccurrences(haystack, needle) {
4077
4151
  if (needle === "") return 0;
@@ -4127,6 +4201,10 @@ var editTool = {
4127
4201
  const { path: path14, oldString, newString, replaceAll = false } = input;
4128
4202
  const safe = resolveInside(ctx.cwd, path14);
4129
4203
  if (!safe.ok) return { content: safe.reason, isError: true };
4204
+ const linkInfo = await lstat3(safe.path).catch(() => null);
4205
+ if (linkInfo?.isSymbolicLink()) {
4206
+ return { content: `Cannot edit ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
4207
+ }
4130
4208
  const info = await stat6(safe.path).catch(() => null);
4131
4209
  if (info && info.size > MAX_FILE_BYTES2) {
4132
4210
  return {
@@ -4244,13 +4322,25 @@ var sensitiveNames = /* @__PURE__ */ new Set([
4244
4322
  ".npmrc",
4245
4323
  ".pypirc",
4246
4324
  ".git-credentials",
4325
+ ".netrc",
4326
+ ".pgpass",
4327
+ ".my.cnf",
4328
+ ".docker/config.json",
4247
4329
  "auth.json",
4248
4330
  "credentials",
4249
4331
  "credentials.json",
4250
4332
  "id_rsa",
4251
- "id_ed25519"
4333
+ "id_ed25519",
4334
+ "id_dsa",
4335
+ "id_ecdsa",
4336
+ "kubeconfig",
4337
+ "known_hosts",
4338
+ "secret.json",
4339
+ "secrets.json",
4340
+ "tokens.json",
4341
+ "service-account.json"
4252
4342
  ]);
4253
- var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx"]);
4343
+ var sensitiveExtensions = /* @__PURE__ */ new Set([".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".p8", ".der"]);
4254
4344
  function isSensitivePath(value) {
4255
4345
  if (typeof value !== "string") return false;
4256
4346
  const normalized = value.replaceAll("\\", "/").toLowerCase();
@@ -4262,7 +4352,7 @@ function isSensitivePath(value) {
4262
4352
  function mentionsSensitivePattern(value) {
4263
4353
  if (typeof value !== "string") return false;
4264
4354
  const lower = value.toLowerCase();
4265
- return isSensitivePath(lower) || lower.includes(".env") || lower.includes(".npmrc") || lower.includes("auth.json") || /\.(?:pem|key|p12|pfx)(?:$|[^a-z0-9])/.test(lower);
4355
+ return isSensitivePath(lower) || lower.includes(".env") || lower.includes(".npmrc") || lower.includes(".netrc") || lower.includes(".pgpass") || lower.includes("auth.json") || lower.includes("credentials") || lower.includes("kubeconfig") || lower.includes("secret") || lower.includes("tokens.json") || /\.(?:pem|key|p12|pfx|jks|keystore|p8|der)(?:$|[^a-z0-9])/.test(lower);
4266
4356
  }
4267
4357
 
4268
4358
  // src/tools/regex.ts
@@ -4539,7 +4629,7 @@ function toToolSchema(tool) {
4539
4629
  }
4540
4630
 
4541
4631
  // src/tools/write.ts
4542
- import { mkdir as mkdir3, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
4632
+ import { lstat as lstat4, mkdir as mkdir3, readFile as readFile11, stat as stat9, writeFile as writeFile7 } from "fs/promises";
4543
4633
  import { dirname as dirname2 } from "path";
4544
4634
  var MAX_FILE_BYTES5 = 5e6;
4545
4635
  var writeTool = {
@@ -4590,6 +4680,10 @@ var writeTool = {
4590
4680
  }
4591
4681
  const safe = resolveInside(ctx.cwd, path14);
4592
4682
  if (!safe.ok) return { content: safe.reason, isError: true };
4683
+ const linkInfo = await lstat4(safe.path).catch(() => null);
4684
+ if (linkInfo?.isSymbolicLink()) {
4685
+ return { content: `Cannot write ${path14}: the path is a symbolic link. Remove the symlink first.`, isError: true };
4686
+ }
4593
4687
  const beforeInfo = await stat9(safe.path).catch(() => null);
4594
4688
  if (beforeInfo && beforeInfo.size > MAX_FILE_BYTES5) {
4595
4689
  return {
@@ -5439,6 +5533,28 @@ async function boot(options) {
5439
5533
  }
5440
5534
  return { removed: providerId, wasActive, ...nextModel ? { nextModel } : {} };
5441
5535
  },
5536
+ async changeProviderKey(providerId, newKey) {
5537
+ const provider = config.providers[providerId];
5538
+ if (!provider) throw new Error(`Provider "${providerId}" is not configured.`);
5539
+ const previousKey = auth[providerId];
5540
+ auth[providerId] = newKey;
5541
+ try {
5542
+ const testRegistry = createRegistry(config, auth);
5543
+ await loadModels(testRegistry.get(providerId));
5544
+ } catch (error) {
5545
+ auth[providerId] = previousKey;
5546
+ throw new Error(
5547
+ `Key verification failed for "${providerId}": ${error instanceof Error ? error.message : String(error)}`
5548
+ );
5549
+ }
5550
+ await saveAuth(auth);
5551
+ registry = createRegistry(config, auth);
5552
+ if (parseModelRef(modelRef)?.provider === providerId) {
5553
+ const models = await loadModels(registry.get(providerId));
5554
+ rememberModels(providerId, models);
5555
+ void refreshModelContextWindow();
5556
+ }
5557
+ },
5442
5558
  sessionId: () => session.id,
5443
5559
  async newSession() {
5444
5560
  await persistQueue.catch(() => void 0);
@@ -5745,6 +5861,17 @@ ${lines.join("\n")}` : null;
5745
5861
  startupUpdateCheck: () => startupUpdate,
5746
5862
  async run(history, hooks, signal) {
5747
5863
  syncMcpTools();
5864
+ const runProviderId = parseModelRef(modelRef)?.provider;
5865
+ if (runProviderId) {
5866
+ try {
5867
+ const models = await loadModels(registry.get(runProviderId));
5868
+ rememberModels(runProviderId, models);
5869
+ } catch (error) {
5870
+ warnings.push(
5871
+ `Could not refresh models for "${runProviderId}": ${error instanceof Error ? error.message : String(error)}`
5872
+ );
5873
+ }
5874
+ }
5748
5875
  let requestHistory = history;
5749
5876
  const budget = createTurnBudget(config.budget, resolvePricing);
5750
5877
  const previousContext = runtime.modelContext();
@@ -6038,7 +6165,7 @@ function validateKey(value) {
6038
6165
  import { render } from "ink";
6039
6166
 
6040
6167
  // src/ui/App.tsx
6041
- import { Box as Box12, useApp } from "ink";
6168
+ import { Box as Box12, Text as Text12, useApp, useInput as useInput2 } from "ink";
6042
6169
  import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
6043
6170
  import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
6044
6171
 
@@ -6080,6 +6207,7 @@ var COMMANDS = [
6080
6207
  { name: "login" },
6081
6208
  { name: "provider" },
6082
6209
  { name: "logout", args: "[provider]" },
6210
+ { name: "key", args: "[provider]" },
6083
6211
  { name: "effort" },
6084
6212
  { name: "thinking" },
6085
6213
  { name: "budget", args: "[tokens]" },
@@ -6167,6 +6295,43 @@ function editDistance(a, b) {
6167
6295
  return previous[b.length] ?? 0;
6168
6296
  }
6169
6297
 
6298
+ // src/ui/App.tsx
6299
+ import TextInput2 from "ink-text-input";
6300
+
6301
+ // src/ui/theme.ts
6302
+ import { createContext, useContext } from "react";
6303
+ var PRESETS = {
6304
+ purple: "#a78bfa",
6305
+ violet: "#8b5cf6",
6306
+ blue: "#60a5fa",
6307
+ cyan: "#22d3ee",
6308
+ green: "#4ade80",
6309
+ orange: "#fb923c",
6310
+ pink: "#f472b6",
6311
+ red: "#f87171",
6312
+ yellow: "#fbbf24",
6313
+ white: "#e5e7eb"
6314
+ };
6315
+ var DEFAULT_ACCENT = PRESETS.purple;
6316
+ function resolveAccent(value) {
6317
+ if (!value) return DEFAULT_ACCENT;
6318
+ const preset = PRESETS[value.toLowerCase()];
6319
+ if (preset) return preset;
6320
+ return /^#[0-9a-f]{6}$/i.test(value) ? value : DEFAULT_ACCENT;
6321
+ }
6322
+ function makeTheme(accent) {
6323
+ return {
6324
+ accent: resolveAccent(accent),
6325
+ ok: PRESETS.green,
6326
+ warn: PRESETS.yellow,
6327
+ error: PRESETS.red
6328
+ };
6329
+ }
6330
+ var ThemeContext = createContext(makeTheme(void 0));
6331
+ function useTheme() {
6332
+ return useContext(ThemeContext);
6333
+ }
6334
+
6170
6335
  // src/ui/components/Confirm.tsx
6171
6336
  import { Box, Text } from "ink";
6172
6337
 
@@ -6200,7 +6365,7 @@ function normalizeHotkey(char) {
6200
6365
  }
6201
6366
 
6202
6367
  // src/ui/i18n.ts
6203
- import { createContext, useContext } from "react";
6368
+ import { createContext as createContext2, useContext as useContext2 } from "react";
6204
6369
  var LANGS = [
6205
6370
  { key: "en", label: "English" },
6206
6371
  { key: "ru", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" }
@@ -6230,6 +6395,7 @@ var en = {
6230
6395
  workingFor: (elapsed) => `working ${elapsed}`,
6231
6396
  titleProvider: "Provider",
6232
6397
  titleLogout: "Sign out of a provider",
6398
+ titleKeyChange: "Change API key for a provider",
6233
6399
  titleSessions: "Resume a session",
6234
6400
  titleSessionAction: "Session action",
6235
6401
  resumed: (id, count) => `Resumed ${id} \u2014 ${count} messages restored`,
@@ -6317,6 +6483,9 @@ var en = {
6317
6483
  logoutAskBody: "Its API key is removed from auth.json and the provider is dropped from the config.",
6318
6484
  loggedOut: (provider) => `Signed out of "${provider}".`,
6319
6485
  logoutNothing: "No provider is signed in.",
6486
+ keyChangeAsk: (provider) => `Change API key for "${provider}"?`,
6487
+ keyChangeAskBody: "The new key replaces the old one. Models are re-fetched automatically.",
6488
+ keyChanged: (provider) => `API key updated for "${provider}".`,
6320
6489
  promptUsage: "Usage: /prompt save <name>",
6321
6490
  promptNothing: "Nothing to save yet \u2014 send a message first.",
6322
6491
  promptSaved: (name) => `Saved prompt "${name}"`,
@@ -6348,6 +6517,7 @@ var en = {
6348
6517
  login: "add a provider (url + key)",
6349
6518
  provider: "switch between configured providers",
6350
6519
  logout: "choose a provider to sign out from",
6520
+ key: "change API key for a provider",
6351
6521
  effort: "set reasoning depth",
6352
6522
  thinking: "toggle reasoning",
6353
6523
  budget: "set token budget per turn",
@@ -6408,6 +6578,7 @@ var ru = {
6408
6578
  workingFor: (elapsed) => `\u0434\u0443\u043C\u0430\u0435\u0442 ${elapsed}`,
6409
6579
  titleProvider: "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
6410
6580
  titleLogout: "\u0412\u044B\u0431\u0440\u0430\u0442\u044C API \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430",
6581
+ titleKeyChange: "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
6411
6582
  titleSessions: "\u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E",
6412
6583
  titleSessionAction: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0441\u0435\u0441\u0441\u0438\u0435\u0439",
6413
6584
  resumed: (id, count) => `\u0421\u0435\u0441\u0441\u0438\u044F ${id} \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439: ${count}`,
@@ -6495,6 +6666,9 @@ var ru = {
6495
6666
  logoutAskBody: "\u041A\u043B\u044E\u0447 \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043B\u0451\u043D \u0438\u0437 auth.json, \u0430 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u2014 \u0438\u0437 \u043A\u043E\u043D\u0444\u0438\u0433\u0430.",
6496
6667
  loggedOut: (provider) => `\u0412\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u0432\u044B\u0445\u043E\u0434 \u0438\u0437 \xAB${provider}\xBB.`,
6497
6668
  logoutNothing: "\u041D\u0438 \u043E\u0434\u0438\u043D \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D.",
6669
+ keyChangeAsk: (provider) => `\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u0434\u043B\u044F \xAB${provider}\xBB?`,
6670
+ keyChangeAskBody: "\u041D\u043E\u0432\u044B\u0439 \u043A\u043B\u044E\u0447 \u0437\u0430\u043C\u0435\u043D\u0438\u0442 \u0441\u0442\u0430\u0440\u044B\u0439. \u041C\u043E\u0434\u0435\u043B\u0438 \u043E\u0431\u043D\u043E\u0432\u044F\u0442\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438.",
6671
+ keyChanged: (provider) => `\u041A\u043B\u044E\u0447 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0434\u043B\u044F \xAB${provider}\xBB.`,
6498
6672
  promptUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /prompt save <\u0438\u043C\u044F>",
6499
6673
  promptNothing: "\u041F\u043E\u043A\u0430 \u043D\u0435\u0447\u0435\u0433\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435.",
6500
6674
  promptSaved: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D`,
@@ -6526,6 +6700,7 @@ var ru = {
6526
6700
  login: "\u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 (url + \u043A\u043B\u044E\u0447)",
6527
6701
  provider: "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043C\u0435\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430\u043C\u0438",
6528
6702
  logout: "\u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0435\u0433\u043E \u043A\u043B\u044E\u0447",
6703
+ key: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430",
6529
6704
  effort: "\u0433\u043B\u0443\u0431\u0438\u043D\u0430 \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u0439",
6530
6705
  thinking: "\u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u044B\u0448\u043B\u0435\u043D\u0438\u044F",
6531
6706
  budget: "\u043B\u0438\u043C\u0438\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0437\u0430 \u0445\u043E\u0434 (0 = \u0431\u0435\u0437\u043B\u0438\u043C\u0438\u0442)",
@@ -6563,43 +6738,9 @@ var DICTIONARIES = { en, ru };
6563
6738
  function stringsFor(lang) {
6564
6739
  return DICTIONARIES[lang ?? "en"];
6565
6740
  }
6566
- var StringsContext = createContext(en);
6741
+ var StringsContext = createContext2(en);
6567
6742
  function useStrings() {
6568
- return useContext(StringsContext);
6569
- }
6570
-
6571
- // src/ui/theme.ts
6572
- import { createContext as createContext2, useContext as useContext2 } from "react";
6573
- var PRESETS = {
6574
- purple: "#a78bfa",
6575
- violet: "#8b5cf6",
6576
- blue: "#60a5fa",
6577
- cyan: "#22d3ee",
6578
- green: "#4ade80",
6579
- orange: "#fb923c",
6580
- pink: "#f472b6",
6581
- red: "#f87171",
6582
- yellow: "#fbbf24",
6583
- white: "#e5e7eb"
6584
- };
6585
- var DEFAULT_ACCENT = PRESETS.purple;
6586
- function resolveAccent(value) {
6587
- if (!value) return DEFAULT_ACCENT;
6588
- const preset = PRESETS[value.toLowerCase()];
6589
- if (preset) return preset;
6590
- return /^#[0-9a-f]{6}$/i.test(value) ? value : DEFAULT_ACCENT;
6591
- }
6592
- function makeTheme(accent) {
6593
- return {
6594
- accent: resolveAccent(accent),
6595
- ok: PRESETS.green,
6596
- warn: PRESETS.yellow,
6597
- error: PRESETS.red
6598
- };
6599
- }
6600
- var ThemeContext = createContext2(makeTheme(void 0));
6601
- function useTheme() {
6602
- return useContext2(ThemeContext);
6743
+ return useContext2(StringsContext);
6603
6744
  }
6604
6745
 
6605
6746
  // src/ui/components/Confirm.tsx
@@ -7376,7 +7517,10 @@ function StatusBar({ status }) {
7376
7517
  }
7377
7518
  )
7378
7519
  ] }),
7379
- details.length > 0 && /* @__PURE__ */ jsx9(Box9, { flexWrap: "wrap", children: /* @__PURE__ */ jsx9(Segments, { items: details }) })
7520
+ details.length > 0 && /* @__PURE__ */ jsx9(Box9, { flexWrap: "wrap", children: details.map((item, index) => /* @__PURE__ */ jsxs9(Fragment2, { children: [
7521
+ index > 0 && /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }),
7522
+ item
7523
+ ] }, index)) })
7380
7524
  ]
7381
7525
  }
7382
7526
  );
@@ -7386,11 +7530,7 @@ function Segments({ items }) {
7386
7530
  const elements = [];
7387
7531
  if (index > 0) {
7388
7532
  elements.push(
7389
- /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
7390
- " ",
7391
- "\xB7",
7392
- " "
7393
- ] }, `sep-${index}`)
7533
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }) }, `sep-${index}`)
7394
7534
  );
7395
7535
  }
7396
7536
  elements.push(/* @__PURE__ */ jsx9(Fragment2, { children: item }, `item-${index}`));
@@ -7476,7 +7616,7 @@ import Spinner2 from "ink-spinner";
7476
7616
  // src/ui/markdown.tsx
7477
7617
  import { Box as Box10, Text as Text10 } from "ink";
7478
7618
  import { useMemo as useMemo2 } from "react";
7479
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
7619
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
7480
7620
  function Markdown({ children }) {
7481
7621
  const blocks = useMemo2(() => extractBlocks(children), [children]);
7482
7622
  return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx10(BlockView, { block }, i)) });
@@ -7489,38 +7629,108 @@ function BlockView({ block }) {
7489
7629
  return /* @__PURE__ */ jsx10(Box10, { marginTop: block.level === 1 ? 1 : 0, children: /* @__PURE__ */ jsx10(Text10, { bold: true, children: truncateBySize(block.text ?? "", size) }) });
7490
7630
  }
7491
7631
  case "blockquote":
7492
- return /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
7493
- "\u2502 ",
7494
- block.text
7495
- ] }) });
7632
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", marginLeft: 2, children: (block.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
7633
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2502 " }),
7634
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: line })
7635
+ ] }, i)) });
7496
7636
  case "ul":
7497
7637
  return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
7498
7638
  /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2022 " }),
7499
- /* @__PURE__ */ jsx10(Text10, { children: inline(item) })
7639
+ /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsx10(BlockView, { block: item }) })
7500
7640
  ] }, i)) });
7501
7641
  case "ol":
7502
7642
  return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: (block.items ?? []).map((item, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
7503
7643
  /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `${i + 1}. ` }),
7504
- /* @__PURE__ */ jsx10(Text10, { children: inline(item) })
7644
+ /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsx10(BlockView, { block: item }) })
7505
7645
  ] }, i)) });
7506
7646
  case "paragraph":
7507
7647
  default:
7508
7648
  return /* @__PURE__ */ jsx10(Text10, { children: inline(block.text ?? "") });
7649
+ case "table":
7650
+ return /* @__PURE__ */ jsx10(TableView, { block });
7651
+ case "code":
7652
+ return /* @__PURE__ */ jsx10(CodeBlock, { lang: block.lang, code: block.text ?? "" });
7653
+ case "hr":
7654
+ return /* @__PURE__ */ jsx10(Box10, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(60) }) });
7655
+ }
7656
+ }
7657
+ function CodeBlock({ lang, code }) {
7658
+ const theme = useTheme();
7659
+ const lines = code.split("\n");
7660
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginTop: 1, children: [
7661
+ lang && /* @__PURE__ */ jsx10(Text10, { dimColor: true, color: theme.accent, children: lang }),
7662
+ /* @__PURE__ */ jsx10(Box10, { borderColor: "gray", borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: line }, i)) }) })
7663
+ ] });
7664
+ }
7665
+ function TableView({ block }) {
7666
+ const headers = block.headers ?? [];
7667
+ const rows = block.rows ?? [];
7668
+ if (headers.length === 0 && rows.length === 0) {
7669
+ return /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "(empty table)" });
7509
7670
  }
7671
+ const colCount = Math.max(headers.length, ...rows.map((r) => r?.length ?? 0));
7672
+ if (colCount === 0) {
7673
+ return /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "(empty table)" });
7674
+ }
7675
+ const colWidths = new Array(colCount).fill(0);
7676
+ const allRows = [headers, ...rows];
7677
+ for (const row of allRows) {
7678
+ for (let i = 0; i < colCount; i++) {
7679
+ const cell = row[i] ?? "";
7680
+ colWidths[i] = Math.max(colWidths[i], [...cell].length);
7681
+ }
7682
+ }
7683
+ const separator = colWidths.map((w) => "\u2500".repeat(w)).join("\u253C");
7684
+ const lines = [];
7685
+ allRows.forEach((row, rowIdx) => {
7686
+ const cells = [];
7687
+ for (let i = 0; i < colCount; i++) {
7688
+ const cell = row[i] ?? "";
7689
+ const align = block.colAligns?.[i] ?? "left";
7690
+ const visualWidth = [...cell].length;
7691
+ const padWidth = colWidths[i] + (cell.length - visualWidth);
7692
+ let padded;
7693
+ if (align === "right") {
7694
+ padded = cell.padStart(padWidth);
7695
+ } else if (align === "center") {
7696
+ const totalPad = padWidth - visualWidth;
7697
+ const left = Math.floor(totalPad / 2);
7698
+ padded = " ".repeat(left) + cell + " ".repeat(totalPad - left);
7699
+ } else {
7700
+ padded = cell.padEnd(padWidth);
7701
+ }
7702
+ cells.push(padded);
7703
+ }
7704
+ lines.push(
7705
+ /* @__PURE__ */ jsx10(Text10, { bold: rowIdx === 0, children: cells.join(" \u2502 ") }, `row-${rowIdx}`)
7706
+ );
7707
+ if (rowIdx === 0) {
7708
+ lines.push(/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: separator }, `sep-${rowIdx}`));
7709
+ }
7710
+ });
7711
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: lines });
7510
7712
  }
7713
+ var CHARS_PER_FONT_LEVEL = 3;
7511
7714
  function truncateBySize(text, size) {
7512
- return text.length > size * 3 ? text.slice(0, size * 3) + "\u2026" : text;
7715
+ const maxLen = size * CHARS_PER_FONT_LEVEL;
7716
+ return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
7513
7717
  }
7514
7718
  var HEADING_RE = /^(#{1,6})\s+(.*)$/;
7515
7719
  var UL_RE = /^([-*+])\s+(.*)$/;
7516
7720
  var OL_RE = /^(\d+)\.\s+(.*)$/;
7517
7721
  var QUOTE_RE = /^>\s?(.*)$/;
7722
+ var TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)$/;
7723
+ var INDENT_RE = /^(\s*)(.*)$/;
7724
+ var HR_RE = /^([-*_])\1{2,}$/;
7725
+ var FENCE_RE = /^(`{3,}|~{3,})(\w*)\s*$/;
7518
7726
  function extractBlocks(src) {
7519
7727
  const lines = src.replace(/\r\n/g, "\n").split("\n");
7520
7728
  const blocks = [];
7521
7729
  let paragraph = [];
7522
7730
  let list = null;
7523
7731
  let quote = [];
7732
+ let code = null;
7733
+ let table = null;
7524
7734
  const flushParagraph = () => {
7525
7735
  if (paragraph.length) {
7526
7736
  blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
@@ -7528,7 +7738,7 @@ function extractBlocks(src) {
7528
7738
  }
7529
7739
  };
7530
7740
  const flushList = () => {
7531
- if (list) {
7741
+ if (list && list.items.length) {
7532
7742
  blocks.push({ type: list.ordered ? "ol" : "ul", items: list.items });
7533
7743
  list = null;
7534
7744
  }
@@ -7539,100 +7749,271 @@ function extractBlocks(src) {
7539
7749
  quote = [];
7540
7750
  }
7541
7751
  };
7752
+ const flushCode = () => {
7753
+ if (code) {
7754
+ blocks.push({ type: "code", lang: code.lang, text: code.lines.join("\n") });
7755
+ code = null;
7756
+ }
7757
+ };
7542
7758
  const flushAll = () => {
7543
7759
  flushParagraph();
7544
7760
  flushList();
7545
7761
  flushQuote();
7762
+ flushCode();
7763
+ };
7764
+ const flushTable = () => {
7765
+ if (table && table.headers.length > 0) {
7766
+ blocks.push({
7767
+ type: "table",
7768
+ headers: table.headers,
7769
+ rows: table.rows,
7770
+ colAligns: table.colAligns
7771
+ });
7772
+ }
7773
+ table = null;
7774
+ };
7775
+ const parseListItem = (text, indent) => {
7776
+ const task = text.match(TASK_RE);
7777
+ if (task) {
7778
+ const done = task[1].toLowerCase() === "x";
7779
+ return {
7780
+ type: "paragraph",
7781
+ text: `${done ? "\u2611" : "\u2610"} ${task[2]}`
7782
+ };
7783
+ }
7784
+ return { type: "paragraph", text };
7546
7785
  };
7547
- for (const line of lines) {
7786
+ for (let i = 0; i < lines.length; i++) {
7787
+ const line = lines[i];
7548
7788
  const trimmed = line.trim();
7789
+ if (code) {
7790
+ const endMatch = trimmed.match(FENCE_RE);
7791
+ if (endMatch && endMatch[1][0] === code.fence[0] && endMatch[1].length >= code.fence.length) {
7792
+ flushCode();
7793
+ continue;
7794
+ }
7795
+ code.lines.push(line);
7796
+ continue;
7797
+ }
7798
+ const fenceMatch = trimmed.match(FENCE_RE);
7799
+ if (fenceMatch) {
7800
+ flushAll();
7801
+ flushTable();
7802
+ code = { fence: fenceMatch[1], lang: fenceMatch[2], lines: [] };
7803
+ continue;
7804
+ }
7549
7805
  if (trimmed === "") {
7806
+ flushTable();
7807
+ flushAll();
7808
+ continue;
7809
+ }
7810
+ const hrMatch = trimmed.match(HR_RE);
7811
+ if (hrMatch) {
7812
+ flushTable();
7550
7813
  flushAll();
7814
+ blocks.push({ type: "hr" });
7551
7815
  continue;
7552
7816
  }
7553
7817
  const heading = trimmed.match(HEADING_RE);
7554
7818
  if (heading) {
7819
+ flushTable();
7555
7820
  flushAll();
7556
7821
  blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() });
7557
7822
  continue;
7558
7823
  }
7559
7824
  const quoteMatch = trimmed.match(QUOTE_RE);
7560
7825
  if (quoteMatch) {
7826
+ flushTable();
7561
7827
  flushParagraph();
7562
7828
  flushList();
7829
+ flushCode();
7563
7830
  quote.push(quoteMatch[1]);
7564
7831
  continue;
7565
7832
  }
7833
+ const indentMatch = line.match(INDENT_RE);
7834
+ const indent = indentMatch?.[1].length ?? 0;
7835
+ const taskMatch = trimmed.match(TASK_RE);
7836
+ if (taskMatch) {
7837
+ flushTable();
7838
+ flushParagraph();
7839
+ flushQuote();
7840
+ flushCode();
7841
+ if (!list || list.ordered || indent !== list.indent) {
7842
+ flushList();
7843
+ list = { ordered: false, items: [], indent };
7844
+ }
7845
+ list.items.push(parseListItem(trimmed, indent));
7846
+ continue;
7847
+ }
7566
7848
  const ulMatch = trimmed.match(UL_RE);
7567
7849
  if (ulMatch) {
7850
+ flushTable();
7568
7851
  flushParagraph();
7569
7852
  flushQuote();
7570
- if (!list || list.ordered) {
7853
+ flushCode();
7854
+ if (!list || list.ordered || indent !== list.indent) {
7571
7855
  flushList();
7572
- list = { ordered: false, items: [] };
7856
+ list = { ordered: false, items: [], indent };
7573
7857
  }
7574
- list.items.push(ulMatch[2].trim());
7858
+ list.items.push(parseListItem(ulMatch[2], indent));
7575
7859
  continue;
7576
7860
  }
7577
7861
  const olMatch = trimmed.match(OL_RE);
7578
7862
  if (olMatch) {
7863
+ flushTable();
7579
7864
  flushParagraph();
7580
7865
  flushQuote();
7581
- if (!list || !list.ordered) {
7866
+ flushCode();
7867
+ if (!list || !list.ordered || indent !== list.indent) {
7582
7868
  flushList();
7583
- list = { ordered: true, items: [] };
7869
+ list = { ordered: true, items: [], indent };
7584
7870
  }
7585
- list.items.push(olMatch[2].trim());
7871
+ list.items.push(parseListItem(olMatch[2], indent));
7586
7872
  continue;
7587
7873
  }
7874
+ const sepMatch = trimmed.match(/^\|?\s*(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?$/);
7875
+ if (sepMatch) {
7876
+ const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
7877
+ if (cells.length >= 2 && cells.every((c) => /^:?-+:?$/.test(c))) {
7878
+ if (table) {
7879
+ table.colAligns = cells.map((c) => {
7880
+ if (c.startsWith(":") && c.endsWith(":")) return "center";
7881
+ if (c.endsWith(":")) return "right";
7882
+ return "left";
7883
+ });
7884
+ continue;
7885
+ }
7886
+ }
7887
+ }
7888
+ const rowMatch = trimmed.match(/^\|?.+\|.+\|?$/);
7889
+ if (rowMatch) {
7890
+ const cells = trimmed.split("|").map((c) => c.trim()).filter((c, idx, arr) => {
7891
+ if (idx === 0 && c === "") return false;
7892
+ if (idx === arr.length - 1 && c === "") return false;
7893
+ return true;
7894
+ });
7895
+ const isWindowsPath = cells.length === 2 && (/^[A-Za-z]:\\/.test(cells[0]) || /^\\\\/.test(cells[0]));
7896
+ if (cells.length >= 2 && cells.every((c) => c.length > 0) && !isWindowsPath) {
7897
+ if (table) {
7898
+ table.rows.push(cells);
7899
+ } else {
7900
+ flushAll();
7901
+ table = { headers: cells, rows: [], colAligns: [] };
7902
+ }
7903
+ continue;
7904
+ }
7905
+ }
7906
+ const asciiSepMatch = trimmed.match(/^[\s\-─┄┈━┅]{3,}$/);
7907
+ if (asciiSepMatch) {
7908
+ if (table && table.headers.length > 0 && table.colAligns.length === 0) {
7909
+ table.colAligns = table.headers.map(() => "left");
7910
+ continue;
7911
+ }
7912
+ if (paragraph.length === 1) {
7913
+ const prevLine = paragraph[0].trim();
7914
+ const tokens3 = prevLine.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
7915
+ if (tokens3.length >= 2 && tokens3.length <= 10) {
7916
+ paragraph = [];
7917
+ flushList();
7918
+ flushQuote();
7919
+ flushCode();
7920
+ table = { headers: tokens3, rows: [], colAligns: tokens3.map(() => "left") };
7921
+ continue;
7922
+ }
7923
+ }
7924
+ }
7925
+ if (table && table.headers.length > 0 && !trimmed.includes("|")) {
7926
+ const tokens3 = trimmed.split(/\s{2,}/).map((c) => c.trim()).filter((c) => c.length > 0);
7927
+ if (tokens3.length >= 2 && tokens3.length <= table.headers.length + 2) {
7928
+ const looksLikeCode = tokens3.some((t) => t.startsWith("//") || t.startsWith("#") || t.startsWith("/*"));
7929
+ const looksLikePath = tokens3.length === 2 && (/^[A-Za-z]:\\/.test(tokens3[0]) || /^\\\\/.test(tokens3[0]));
7930
+ if (!looksLikeCode && !looksLikePath) {
7931
+ table.rows.push(tokens3);
7932
+ continue;
7933
+ }
7934
+ }
7935
+ }
7936
+ flushTable();
7588
7937
  flushList();
7589
7938
  flushQuote();
7939
+ flushCode();
7590
7940
  paragraph.push(trimmed);
7591
7941
  }
7942
+ flushTable();
7592
7943
  flushAll();
7593
7944
  return blocks;
7594
7945
  }
7595
- function inline(text) {
7946
+ var MAX_INLINE_DEPTH = 10;
7947
+ function inline(text, depth = 0) {
7948
+ if (depth > MAX_INLINE_DEPTH) {
7949
+ return text;
7950
+ }
7596
7951
  const nodes = [];
7597
7952
  let rest = text;
7598
7953
  let key = 0;
7599
- const RE = /(`+)([^`]+?)\1|(\*\*)(.+?)\2|(~~)(.+?)\4|(\*)(.+?)\6|(__)(.+?)\8/;
7954
+ const matchers = [
7955
+ {
7956
+ re: /(!\[)([^\]]*)\]\(([^)]+)\)/,
7957
+ handler: (m) => /* @__PURE__ */ jsxs10(Text10, { color: "cyan", bold: true, children: [
7958
+ "[img: ",
7959
+ m[2],
7960
+ "]"
7961
+ ] }, key++)
7962
+ },
7963
+ {
7964
+ re: /(\[)([^\]]*)\]\(([^)]+)\)/,
7965
+ handler: (m) => /* @__PURE__ */ jsxs10(Fragment4, { children: [
7966
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", underline: true, children: inline(m[2], depth + 1) }, key++),
7967
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "gray", children: [
7968
+ "(",
7969
+ m[3],
7970
+ ")"
7971
+ ] }, key++)
7972
+ ] })
7973
+ },
7974
+ {
7975
+ re: /(`+)([^`]+?)\1/,
7976
+ handler: (m) => /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
7977
+ },
7978
+ {
7979
+ re: /(\*\*)(.+?)\1/,
7980
+ handler: (m) => /* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
7981
+ },
7982
+ {
7983
+ re: /(__)(.+?)\1/,
7984
+ handler: (m) => /* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[2], depth + 1) }, key++)
7985
+ },
7986
+ {
7987
+ re: /(~~)(.+?)\1/,
7988
+ handler: (m) => /* @__PURE__ */ jsx10(Text10, { strikethrough: true, children: m[2] }, key++)
7989
+ },
7990
+ {
7991
+ re: /(\*)(.+?)\1/,
7992
+ handler: (m) => /* @__PURE__ */ jsx10(Text10, { italic: true, children: inline(m[2], depth + 1) }, key++)
7993
+ }
7994
+ ];
7600
7995
  while (rest.length > 0) {
7601
- const m = rest.match(RE);
7602
- if (!m) {
7996
+ let best = null;
7997
+ for (const { re, handler } of matchers) {
7998
+ const m = rest.match(re);
7999
+ if (m && (best === null || (m.index ?? 0) < (best.match.index ?? 0))) {
8000
+ best = { match: m, handler };
8001
+ }
8002
+ }
8003
+ if (!best) {
7603
8004
  nodes.push(rest);
7604
8005
  break;
7605
8006
  }
7606
- const idx = m.index ?? 0;
8007
+ const idx = best.match.index ?? 0;
7607
8008
  if (idx > 0) nodes.push(rest.slice(0, idx));
7608
- if (m[1]) {
7609
- nodes.push(
7610
- /* @__PURE__ */ jsx10(Text10, { bold: true, color: "cyan", children: m[2] }, key++)
7611
- );
7612
- } else if (m[3]) {
7613
- nodes.push(
7614
- /* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[4]) }, key++)
7615
- );
7616
- } else if (m[5]) {
7617
- nodes.push(
7618
- /* @__PURE__ */ jsx10(Text10, { strikethrough: true, children: m[6] }, key++)
7619
- );
7620
- } else if (m[7]) {
7621
- nodes.push(
7622
- /* @__PURE__ */ jsx10(Text10, { italic: true, children: inline(m[8]) }, key++)
7623
- );
7624
- } else if (m[9]) {
7625
- nodes.push(
7626
- /* @__PURE__ */ jsx10(Text10, { bold: true, children: inline(m[10]) }, key++)
7627
- );
7628
- }
7629
- rest = rest.slice(idx + m[0].length);
8009
+ nodes.push(best.handler(best.match));
8010
+ rest = rest.slice(idx + best.match[0].length);
7630
8011
  }
7631
8012
  return nodes.length === 1 ? nodes[0] : nodes;
7632
8013
  }
7633
8014
 
7634
8015
  // src/ui/components/Transcript.tsx
7635
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
8016
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
7636
8017
  var HEADER = { kind: "header" };
7637
8018
  var Transcript = memo2(function Transcript2({ bubbles, workspace }) {
7638
8019
  const liveAt = bubbles.findLastIndex(
@@ -7699,7 +8080,7 @@ function ToolView({ bubble }) {
7699
8080
  mark,
7700
8081
  " ",
7701
8082
  bubble.summary,
7702
- hasChanges && /* @__PURE__ */ jsxs11(Fragment4, { children: [
8083
+ hasChanges && /* @__PURE__ */ jsxs11(Fragment5, { children: [
7703
8084
  diff.added > 0 && /* @__PURE__ */ jsx11(Text11, { color: theme.ok, bold: true, children: ` +${diff.added}` }),
7704
8085
  diff.removed > 0 && /* @__PURE__ */ jsx11(Text11, { color: theme.error, bold: true, children: ` -${diff.removed}` })
7705
8086
  ] })
@@ -8078,7 +8459,7 @@ function App({
8078
8459
  return () => clearInterval(timer);
8079
8460
  }, [turnStart]);
8080
8461
  useEffect2(() => {
8081
- if (process.platform === "darwin") return;
8462
+ if (process.platform === "darwin" || process.platform === "win32") return;
8082
8463
  let last = "";
8083
8464
  let cancelling = false;
8084
8465
  let giveUp = false;
@@ -8152,6 +8533,12 @@ function App({
8152
8533
  }),
8153
8534
  []
8154
8535
  );
8536
+ const promptInput = useCallback(
8537
+ (title, mask) => new Promise((resolve3) => {
8538
+ setOverlay({ kind: "textinput", title, mask, resolve: resolve3, cancel: () => resolve3(null) });
8539
+ }),
8540
+ []
8541
+ );
8155
8542
  const runAgent = useCallback(async () => {
8156
8543
  const controller = new AbortController();
8157
8544
  abort.current = controller;
@@ -8469,6 +8856,35 @@ ${strings.modelSet(result.nextModel)}` : ""}`;
8469
8856
  forceRender((n) => n + 1);
8470
8857
  return;
8471
8858
  }
8859
+ case "key": {
8860
+ const providers = runtime.listProviderItems();
8861
+ if (providers.length === 0) {
8862
+ notice("warn", strings.logoutNothing);
8863
+ return;
8864
+ }
8865
+ let providerId = rest[0];
8866
+ if (!providerId) {
8867
+ providerId = providers.length === 1 ? providers[0]?.key : await pick(strings.titleKeyChange, providers) ?? void 0;
8868
+ }
8869
+ if (!providerId) return;
8870
+ if (!providers.some((provider) => provider.key === providerId)) {
8871
+ notice("warn", `Provider "${providerId}" is not configured.`);
8872
+ return;
8873
+ }
8874
+ if (!await ask2(strings.keyChangeAsk(providerId), strings.keyChangeAskBody, true)) return;
8875
+ const newKey = await promptInput(strings.apiKey, true);
8876
+ if (!newKey || newKey.trim() === "") {
8877
+ notice("warn", "Empty key \u2014 nothing changed.");
8878
+ return;
8879
+ }
8880
+ try {
8881
+ await runtime.changeProviderKey(providerId, newKey.trim());
8882
+ notice("info", strings.keyChanged(providerId));
8883
+ } catch (error) {
8884
+ notice("error", error instanceof Error ? error.message : String(error));
8885
+ }
8886
+ return;
8887
+ }
8472
8888
  case "lang": {
8473
8889
  const choice = await pick(
8474
8890
  strings.titleLang,
@@ -9202,6 +9618,21 @@ ${strings.configAt(runtime.configPath())}`);
9202
9618
  }
9203
9619
  }
9204
9620
  ),
9621
+ overlay.kind === "textinput" && /* @__PURE__ */ jsx12(
9622
+ TextInputOverlay,
9623
+ {
9624
+ title: overlay.title,
9625
+ mask: overlay.mask,
9626
+ onAnswer: (value) => {
9627
+ overlay.resolve(value);
9628
+ setOverlay({ kind: "none" });
9629
+ },
9630
+ onCancel: () => {
9631
+ overlay.cancel?.();
9632
+ setOverlay({ kind: "none" });
9633
+ }
9634
+ }
9635
+ ),
9205
9636
  overlay.kind === "none" && /* @__PURE__ */ jsx12(
9206
9637
  PromptInput,
9207
9638
  {
@@ -9242,6 +9673,34 @@ ${strings.configAt(runtime.configPath())}`);
9242
9673
  ] })
9243
9674
  );
9244
9675
  }
9676
+ function TextInputOverlay({
9677
+ title,
9678
+ mask,
9679
+ onAnswer,
9680
+ onCancel
9681
+ }) {
9682
+ const [value, setValue] = useState5("");
9683
+ const theme = useTheme();
9684
+ const submit = useCallback(() => {
9685
+ onAnswer(value || null);
9686
+ }, [value, onAnswer]);
9687
+ useInput2((_input, key) => {
9688
+ if (key.escape) onCancel();
9689
+ });
9690
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
9691
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: title }),
9692
+ /* @__PURE__ */ jsx12(
9693
+ TextInput2,
9694
+ {
9695
+ value,
9696
+ onChange: setValue,
9697
+ onSubmit: submit,
9698
+ mask: mask ? "\u2022" : void 0
9699
+ }
9700
+ ),
9701
+ /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, color: theme.accent, children: "enter submit \xB7 esc cancel" }) })
9702
+ ] });
9703
+ }
9245
9704
  function slashNeedsIdle(line) {
9246
9705
  const [command = "", subcommand = ""] = line.slice(1).trim().toLowerCase().split(/\s+/);
9247
9706
  return NEEDS_IDLE.has(command) || command === "mcp" && (subcommand === "add" || subcommand === "delete" || subcommand === "enable" || subcommand === "disable");
@@ -9323,6 +9782,13 @@ function copyToClipboard(text) {
9323
9782
  }
9324
9783
  return;
9325
9784
  }
9785
+ if (process.platform === "win32") {
9786
+ try {
9787
+ run("clip", []);
9788
+ } catch {
9789
+ }
9790
+ return;
9791
+ }
9326
9792
  const candidates = [
9327
9793
  ["wl-copy", []],
9328
9794
  ["xclip", ["-selection", "clipboard"]],