@kernelonpanic/kitcode 1.4.3 → 1.4.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/README.md CHANGED
@@ -114,6 +114,8 @@ Press `/` to open the command list.
114
114
  | `/bell [on\|off]` | Toggle the beep when a long answer finishes or a permission prompt appears. |
115
115
  | `/context [tokens]` | Show or set the model context window used by the meter and automatic compaction. |
116
116
  | `/memory show` · `/memory set <text>` · `/memory clear` | View, replace, or clear this project's persistent notes. |
117
+ | `/systemp [show]` · `/systemp set <text>` · `/systemp load <path>` · `/systemp on\|off` · `/systemp clear` | Append custom instructions to the system prompt; toggle without deleting. |
118
+ | `/steps [0-1000]` | Cap model calls per turn (default 256, 0 = unlimited). |
117
119
  | `/resume` · `/clear` | Resume a session or start a new one. |
118
120
  | `/sessions` | Search sessions, then resume, rename, delete, or export one. |
119
121
  | `/sessions list` | List all saved sessions. |
@@ -213,6 +215,24 @@ and `max` to `high`; for GPT-5.2–5.4, it maps `max` to `xhigh`. It omits effor
213
215
  OpenAI-compatible model names. The Anthropic adapter sends the resolved level in `output_config`.
214
216
  Supported levels and their effect depend on the model and provider.
215
217
 
218
+ ## Custom system prompt
219
+
220
+ `/systemp` appends your own instructions to the built-in system prompt on every turn. Set it once and it persists in the config:
221
+
222
+ ```text
223
+ /systemp set Always answer in Russian. Never add comments that restate the code.
224
+ /systemp load ./my-prompt.txt
225
+ /systemp off
226
+ /systemp on
227
+ /systemp clear
228
+ ```
229
+
230
+ `off` keeps the text in the config but stops sending it; `clear` removes it entirely. The addition appears at the end of the system prompt, after the built-in instructions and project memory.
231
+
232
+ ## Turn step cap
233
+
234
+ `/steps` shows the current cap on model calls per turn (default 256). A turn that hits the cap stops with a warning instead of looping forever. Set `/steps 0` for no cap — the turn budget and token limits still apply.
235
+
216
236
  ## Project memory
217
237
 
218
238
  Project notes persist across chats, `/clear`, and restarts. Manage them with:
@@ -282,8 +302,8 @@ Minimal manual provider configuration:
282
302
  }
283
303
  ```
284
304
 
285
- The main settings are `model`, `effort`, `thinking`, `contextWindow`, `maxTokens`, `budget`,
286
- `diagnostics`, `theme`, `permissions`, `providers`, and `mcp`. `budget` controls local per-message
305
+ The main settings are `model`, `effort`, `thinking`, `contextWindow`, `maxTokens`, `maxTurnSteps`,
306
+ `systemPrompt`, `systemPromptEnabled`, `budget`, `diagnostics`, `theme`, `permissions`, `providers`, and `mcp`. `budget` controls local per-message
287
307
  safety limits for model requests, tokens, estimated cost (when model pricing is available), and
288
308
  subagents. These values are
289
309
  not the provider's account balance or rate limits. `diagnostics.autoRun`
package/README.ru.md CHANGED
@@ -115,6 +115,8 @@ http://localhost:11434/v1
115
115
  | `/bell [on\|off]` | Включить/выключить сигнал о готовом долгом ответе и запросе разрешения. |
116
116
  | `/context [tokens\|auto]` | Показать или задать окно контекста модели для индикатора и автосжатия. |
117
117
  | `/memory show` · `/memory set <текст>` · `/memory clear` | Просмотреть, заменить или очистить постоянные заметки проекта. |
118
+ | `/systemp [show]` · `/systemp set <текст>` · `/systemp load <путь>` · `/systemp on\|off` · `/systemp clear` | Добавить свои инструкции к системному промпту; включать и выключать без удаления. |
119
+ | `/steps [0-1000]` | Ограничить число вызовов модели за ход (по умолчанию 256, 0 = без лимита). |
118
120
  | `/resume` · `/clear` | Восстановить сессию или начать новую. |
119
121
  | `/sessions` | Найти сессию и открыть, переименовать, удалить или экспортировать её. |
120
122
  | `/sessions list` | Показать все сохранённые сессии. |
@@ -225,6 +227,24 @@ scrollback терминала, поэтому историю можно плав
225
227
  OpenAI-совместимых моделей параметр не отправляется. Anthropic получает выбранный уровень в
226
228
  `output_config`. Реальная поддержка зависит от модели и провайдера.
227
229
 
230
+ ## Дополнение к системному промпту
231
+
232
+ `/systemp` добавляет свои инструкции к встроенному системному промпту на каждый ход. Настройка сохраняется в конфиге:
233
+
234
+ ```text
235
+ /systemp set Всегда отвечай по-русски. Не добавляй комментарии, повторяющие код.
236
+ /systemp load ./my-prompt.txt
237
+ /systemp off
238
+ /systemp on
239
+ /systemp clear
240
+ ```
241
+
242
+ `off` сохраняет текст в конфиге, но не отправляет его; `clear` удаляет полностью. Дополнение идёт в конец системного промпта, после встроенных инструкций и памяти проекта.
243
+
244
+ ## Лимит вызовов модели за ход
245
+
246
+ `/steps` показывает текущий лимит вызовов модели за ход (по умолчанию 256). Ход, достигший лимита, останавливается с предупреждением вместо бесконечного цикла. `/steps 0` снимает лимит полностью — бюджет токенов при этом продолжает действовать.
247
+
228
248
  ## Память проекта
229
249
 
230
250
  Заметки проекта сохраняются между чатами, `/clear` и перезапусками:
@@ -281,8 +301,8 @@ OpenAI-совместимых моделей параметр не отправ
281
301
  }
282
302
  ```
283
303
 
284
- Основные параметры: `model`, `effort`, `thinking`, `contextWindow`, `maxTokens`, `budget` и
285
- `diagnostics`, `theme`, `permissions`, `providers` и `mcp`. Секция `budget` задаёт лимиты запросов к
304
+ Основные параметры: `model`, `effort`, `thinking`, `contextWindow`, `maxTokens`, `maxTurnSteps`,
305
+ `systemPrompt`, `systemPromptEnabled`, `budget`, `diagnostics`, `theme`, `permissions`, `providers` и `mcp`. Секция `budget` задаёт лимиты запросов к
286
306
  модели, токенов, примерной стоимости (если провайдер сообщает цены) и субагентов на одно сообщение.
287
307
  `diagnostics.autoRun` включает проверки после правок, а `diagnostics.commands` позволяет заменить
288
308
  автоопределение списком до восьми команд. При каждом входе KitCode проверяет новую версию в npm и,
package/dist/index.js CHANGED
@@ -81,6 +81,14 @@ var configSchema = z.object({
81
81
  // Manual override for the model's context window. Unset means the window
82
82
  // comes from the provider catalogue or the endpoint's model list.
83
83
  contextWindow: z.number().int().min(1e3).max(1e7).optional(),
84
+ // Extra instructions appended to the built-in system prompt on every turn.
85
+ systemPrompt: z.string().max(2e4).optional(),
86
+ // When false, systemPrompt is kept in the config but not sent to the model.
87
+ systemPromptEnabled: z.boolean().default(true),
88
+ // Hard cap on model calls in one turn. Higher values let large refactors
89
+ // finish; 0 disables the cap entirely (use with care — a runaway loop will
90
+ // keep burning tokens until you press Esc).
91
+ maxTurnSteps: z.number().int().min(0).max(1e3).default(256),
84
92
  maxTokens: z.number().int().positive().max(2e5).default(64e3),
85
93
  budget: budgetSchema.default({
86
94
  maxTokensPerTurn: 1e7,
@@ -1888,7 +1896,7 @@ function validSessionId(id) {
1888
1896
  var MAX_PAUSE_RESUMES = 5;
1889
1897
  var MAX_RETRIES2 = 3;
1890
1898
  var INITIAL_BACKOFF_MS = 1e3;
1891
- var MAX_TURN_STEPS = 64;
1899
+ var DEFAULT_MAX_TURN_STEPS = 256;
1892
1900
  function createExecutionLane() {
1893
1901
  let tail2 = Promise.resolve();
1894
1902
  return {
@@ -1900,6 +1908,8 @@ function createExecutionLane() {
1900
1908
  };
1901
1909
  }
1902
1910
  async function runTurn(cfg, history, hooks, signal) {
1911
+ const maxTurnSteps = cfg.maxTurnSteps ?? DEFAULT_MAX_TURN_STEPS;
1912
+ const hasStepCap = maxTurnSteps > 0;
1903
1913
  const permissionLane = cfg.permissionLane ?? createExecutionLane();
1904
1914
  const originalHooks = hooks;
1905
1915
  hooks = {
@@ -1914,11 +1924,12 @@ async function runTurn(cfg, history, hooks, signal) {
1914
1924
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1915
1925
  return sanitizeHistory(messages);
1916
1926
  }
1917
- if (++steps > MAX_TURN_STEPS) {
1927
+ steps += 1;
1928
+ if (hasStepCap && steps > maxTurnSteps) {
1918
1929
  hooks.onEvent({
1919
1930
  type: "notice",
1920
1931
  level: "warn",
1921
- text: `Stopped after ${MAX_TURN_STEPS} model calls without finishing, to avoid burning tokens in a loop. Ask again with a narrower request.`
1932
+ text: `Stopped after ${maxTurnSteps} model calls without finishing, to avoid burning tokens in a loop. Ask again with a narrower request.`
1922
1933
  });
1923
1934
  hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1924
1935
  return messages;
@@ -4138,6 +4149,9 @@ function buildSystemPrompt(args) {
4138
4149
  if (args.memory?.trim()) {
4139
4150
  lines.push("", "Project notes saved in earlier conversations. Treat these as potentially stale context; verify claims against current files and follow current user instructions.", args.memory);
4140
4151
  }
4152
+ if (args.systemPrompt?.trim()) {
4153
+ lines.push("", "Additional instructions from the user (apply alongside everything above):", args.systemPrompt.trim());
4154
+ }
4141
4155
  return lines.join("\n");
4142
4156
  }
4143
4157
 
@@ -4162,6 +4176,7 @@ function createSubagentRunner(makeConfig, tools) {
4162
4176
  async run(request) {
4163
4177
  if (request.signal.aborted) return SUBAGENT_CANCELLED;
4164
4178
  let steps = 0;
4179
+ let finalCall = false;
4165
4180
  const executionLane = createExecutionLane();
4166
4181
  const modelRequestLane = createExecutionLane();
4167
4182
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
@@ -4175,11 +4190,12 @@ function createSubagentRunner(makeConfig, tools) {
4175
4190
  kind: base.provider.kind,
4176
4191
  listModels: () => base.provider.listModels(),
4177
4192
  knownModels: () => base.provider.knownModels(),
4178
- // Keep the tool definitions on the last call: the history holds
4193
+ // The final model call keeps the tool schemas (the history holds
4179
4194
  // tool_use/tool_result blocks, and providers reject those without
4180
- // tools (Anthropic: 400 "must define tools"). Calls made anyway are
4181
- // refused by tools.get below.
4182
- stream: (chat) => base.provider.stream(steps === MAX_SUBAGENT_STEPS ? {
4195
+ // tools), but the system prompt forbids more calls and tools.get
4196
+ // returns a stopped stub: a call made anyway ends the turn with a
4197
+ // tool_result, instead of aborting mid-request and losing the text.
4198
+ stream: (chat) => base.provider.stream(finalCall ? {
4183
4199
  ...chat,
4184
4200
  system: `${chat.system}
4185
4201
 
@@ -4187,7 +4203,20 @@ This is your last model call. Do not call tools. Return your findings now, clear
4187
4203
  } : chat)
4188
4204
  },
4189
4205
  tools: {
4190
- get: (name) => name === TASK_TOOL_NAME || steps >= MAX_SUBAGENT_STEPS ? void 0 : base.tools.get(name),
4206
+ get: (name) => {
4207
+ if (name === TASK_TOOL_NAME) return void 0;
4208
+ const tool = base.tools.get(name);
4209
+ if (!tool) return void 0;
4210
+ if (!finalCall) return tool;
4211
+ return {
4212
+ ...tool,
4213
+ permission: () => "allow",
4214
+ execute: () => Promise.resolve({
4215
+ content: "Tool budget exhausted: this was your last model call. Write the final answer now from what you already gathered.",
4216
+ isError: true
4217
+ })
4218
+ };
4219
+ },
4191
4220
  schemas: () => base.tools.schemas().filter((schema) => schema.name !== TASK_TOOL_NAME)
4192
4221
  },
4193
4222
  system: `${SUBAGENT_SYSTEM}
@@ -4198,7 +4227,11 @@ Working directory: ${base.cwd}`
4198
4227
  const signal = AbortSignal.any([request.signal, limit.signal]);
4199
4228
  const hooks = {
4200
4229
  onEvent(event) {
4201
- if (event.type === "turn_start" && ++steps > MAX_SUBAGENT_STEPS) limit.abort();
4230
+ if (event.type === "turn_start") {
4231
+ ++steps;
4232
+ if (steps > MAX_SUBAGENT_STEPS + 1) limit.abort();
4233
+ else if (steps >= MAX_SUBAGENT_STEPS) finalCall = true;
4234
+ }
4202
4235
  if (event.type === "tool_start") request.onProgress(brief(event.summary, 80));
4203
4236
  if (event.type === "subagent_start" || event.type === "subagent_end" || event.type === "subagent_event") return;
4204
4237
  request.onEvent?.(event);
@@ -4413,7 +4446,7 @@ function clip(value) {
4413
4446
  // package.json
4414
4447
  var package_default = {
4415
4448
  name: "@kernelonpanic/kitcode",
4416
- version: "1.4.3",
4449
+ version: "1.4.5",
4417
4450
  description: "Terminal coding agent with a config you never have to write by hand",
4418
4451
  type: "module",
4419
4452
  license: "MIT",
@@ -4481,7 +4514,7 @@ var package_default = {
4481
4514
 
4482
4515
  // src/version.ts
4483
4516
  var KITCODE_VERSION = package_default.version;
4484
- var KITCODE_COMMIT = true ? "586eb4def64a8ef824bf2fe2f58a9f71e18090f0" : "development";
4517
+ var KITCODE_COMMIT = true ? "aac7d92efa29456e6757bdef12abafb489064dbe" : "development";
4485
4518
  function kitcodeUserAgent(sdkUserAgent2) {
4486
4519
  return `KitCode/${KITCODE_VERSION} ${sdkUserAgent2}`.trim();
4487
4520
  }
@@ -7292,7 +7325,8 @@ async function boot(options) {
7292
7325
  cwd: options.cwd,
7293
7326
  toolNames: tools.list().map((tool) => tool.name),
7294
7327
  skills: skillCatalogue,
7295
- memory: projectMemory
7328
+ memory: projectMemory,
7329
+ systemPrompt: config.systemPromptEnabled ? config.systemPrompt : void 0
7296
7330
  });
7297
7331
  let modelRef = session.model || config.model || "";
7298
7332
  if (options.modelRef) modelRef = options.modelRef;
@@ -7362,6 +7396,7 @@ async function boot(options) {
7362
7396
  maxTokens: config.maxTokens,
7363
7397
  effort: config.effort,
7364
7398
  thinking: config.thinking,
7399
+ maxTurnSteps: config.maxTurnSteps,
7365
7400
  budget: activeBudget,
7366
7401
  checkpoint: activeCheckpoint,
7367
7402
  trimContext: (messages) => trimForWindow(resolved.provider.kind, messages)
@@ -7742,6 +7777,31 @@ async function boot(options) {
7742
7777
  draft.lang = lang;
7743
7778
  });
7744
7779
  },
7780
+ getSystemPrompt: () => config.systemPrompt ?? "",
7781
+ isSystemPromptEnabled: () => config.systemPromptEnabled && config.systemPrompt !== void 0 && config.systemPrompt.trim() !== "",
7782
+ async setSystemPrompt(text) {
7783
+ const trimmed = text.trim();
7784
+ await persistConfig((draft) => {
7785
+ if (trimmed === "") delete draft.systemPrompt;
7786
+ else draft.systemPrompt = trimmed;
7787
+ });
7788
+ },
7789
+ async setSystemPromptEnabled(enabled) {
7790
+ await persistConfig((draft) => {
7791
+ draft.systemPromptEnabled = enabled;
7792
+ });
7793
+ },
7794
+ async loadSystemPromptFile(requestedPath) {
7795
+ const { readFile: readFile15 } = await import("fs/promises");
7796
+ const resolved = path15.resolve(options.cwd, requestedPath);
7797
+ return readFile15(resolved, "utf8");
7798
+ },
7799
+ getMaxTurnSteps: () => config.maxTurnSteps,
7800
+ async setMaxTurnSteps(steps) {
7801
+ await persistConfig((draft) => {
7802
+ draft.maxTurnSteps = steps;
7803
+ });
7804
+ },
7745
7805
  getAccent: () => config.theme.accent,
7746
7806
  async setAccent(value) {
7747
7807
  await persistConfig((draft) => {
@@ -8322,7 +8382,7 @@ function validateKey(value) {
8322
8382
  import { render } from "ink";
8323
8383
 
8324
8384
  // src/ui/App.tsx
8325
- import { Box as Box14, Text as Text13, useApp, useInput as useInput3, useStdout as useStdout2 } from "ink";
8385
+ import { Box as Box14, Text as Text13, useApp, useInput as useInput4, useStdout as useStdout2 } from "ink";
8326
8386
  import { useCallback, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef5, useState as useState8 } from "react";
8327
8387
 
8328
8388
  // src/ui/terminal-size.ts
@@ -8508,6 +8568,8 @@ var COMMANDS = [
8508
8568
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
8509
8569
  { name: "attach", args: "<path|clipboard|clear>" },
8510
8570
  { name: "memory", args: "[show|set <text>|clear]" },
8571
+ { name: "systemp", args: "[set <text>|clear]", ...BUSY_SAFE },
8572
+ { name: "steps", args: "[count]", ...BUSY_SAFE },
8511
8573
  { name: "compact" },
8512
8574
  { name: "update" },
8513
8575
  { name: "checker", ...BUSY_SAFE },
@@ -8881,6 +8943,17 @@ ${url}`,
8881
8943
  memoryEmpty: "Project memory is empty.",
8882
8944
  memoryClearAsk: "Clear project memory?",
8883
8945
  memoryReplaceAsk: "Replace all project memory notes with this text?",
8946
+ systemPromptTitle: "System prompt addition",
8947
+ systemPromptUsage: "/systemp [show] \xB7 /systemp set <text> \xB7 /systemp clear",
8948
+ systemPromptEmpty: "No extra system prompt. Set one with /systemp set <text>.",
8949
+ systemPromptSet: "System prompt addition saved.",
8950
+ systemPromptCleared: "System prompt addition cleared.",
8951
+ systemPromptEnabled: "System prompt enabled.",
8952
+ systemPromptDisabled: "System prompt disabled (kept in config).",
8953
+ stepsCurrent: (n) => n === 0 ? "Model calls per turn: unlimited" : `Model calls per turn: ${n}`,
8954
+ stepsSet: (n) => `Model calls per turn set to ${n}`,
8955
+ stepsUnlimited: "Model calls per turn: unlimited (0). The turn budget still applies.",
8956
+ stepsInvalid: "Usage: /steps <0-1000> (0 = unlimited)",
8884
8957
  promptDeleted: (name) => `Deleted prompt "${name}"`,
8885
8958
  promptNotFound: (name) => `Prompt "${name}" not found.`,
8886
8959
  promptsEmpty: "No saved prompts. Use /prompt save <name>.",
@@ -8923,6 +8996,8 @@ ${url}`,
8923
8996
  theme: "change the accent colour",
8924
8997
  lang: "change the interface language",
8925
8998
  memory: "view, replace or clear project memory",
8999
+ systemp: "view, set, load from file, or toggle the extra system prompt",
9000
+ steps: "cap model calls per turn",
8926
9001
  prompt: "insert or save a prompt",
8927
9002
  "prompt save": "save the current or recent text as a prompt",
8928
9003
  "prompt delete": "delete a saved prompt",
@@ -9118,6 +9193,17 @@ ${url}`,
9118
9193
  memoryEmpty: "\u041F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0443\u0441\u0442\u0430.",
9119
9194
  memoryClearAsk: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430?",
9120
9195
  memoryReplaceAsk: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435 \u0437\u0430\u043C\u0435\u0442\u043A\u0438 \u043F\u0430\u043C\u044F\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u044D\u0442\u0438\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C?",
9196
+ systemPromptTitle: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443",
9197
+ systemPromptUsage: "/systemp [show] \xB7 /systemp set <\u0442\u0435\u043A\u0441\u0442> \xB7 /systemp clear",
9198
+ systemPromptEmpty: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u043D\u0435\u0442. \u0417\u0430\u0434\u0430\u0439 \u0447\u0435\u0440\u0435\u0437 /systemp set <\u0442\u0435\u043A\u0441\u0442>.",
9199
+ systemPromptSet: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E.",
9200
+ systemPromptCleared: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0443\u0434\u0430\u043B\u0435\u043D\u043E.",
9201
+ systemPromptEnabled: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E.",
9202
+ systemPromptDisabled: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E (\u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0435).",
9203
+ stepsCurrent: (n) => n === 0 ? "\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434: \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430" : `\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434: ${n}`,
9204
+ stepsSet: (n) => `\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${n}`,
9205
+ stepsUnlimited: "\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434: \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430 (0). \u0411\u044E\u0434\u0436\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0432\u0441\u0451 \u0435\u0449\u0451 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442.",
9206
+ stepsInvalid: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /steps <0-1000> (0 = \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430)",
9121
9207
  promptDeleted: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
9122
9208
  promptNotFound: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.`,
9123
9209
  promptsEmpty: "\u0421\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u043F\u0440\u043E\u043C\u0442\u043E\u0432 \u043D\u0435\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 /prompt save <\u0438\u043C\u044F>.",
@@ -9160,6 +9246,8 @@ ${url}`,
9160
9246
  theme: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0430\u043A\u0446\u0435\u043D\u0442\u0430",
9161
9247
  lang: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u044F\u0437\u044B\u043A \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430",
9162
9248
  memory: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430",
9249
+ systemp: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u0437\u0430\u0434\u0430\u0442\u044C, \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0438\u0437 \u0444\u0430\u0439\u043B\u0430 \u0438\u043B\u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0434\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443",
9250
+ steps: "\u043B\u0438\u043C\u0438\u0442 \u0432\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434",
9163
9251
  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",
9164
9252
  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",
9165
9253
  "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",
@@ -9338,18 +9426,30 @@ function LanguagePicker({ onPick }) {
9338
9426
  }
9339
9427
 
9340
9428
  // src/ui/components/Onboarding.tsx
9341
- import { Box as Box4, Text as Text4 } from "ink";
9429
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
9342
9430
  import Spinner from "ink-spinner";
9343
9431
  import TextInput from "ink-text-input";
9344
9432
  import { useState as useState2 } from "react";
9345
9433
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
9346
- function Onboarding({ onSubmit }) {
9434
+ function Onboarding({
9435
+ onSubmit,
9436
+ onCancel
9437
+ }) {
9347
9438
  const theme = useTheme();
9348
9439
  const strings = useStrings();
9349
9440
  const [step, setStep] = useState2("url");
9350
9441
  const [url, setUrl] = useState2("");
9351
9442
  const [key, setKey] = useState2("");
9352
9443
  const [error, setError] = useState2("");
9444
+ useInput2((_input, inputKey) => {
9445
+ if (!inputKey.escape || step === "working") return;
9446
+ if (step === "key") {
9447
+ setError("");
9448
+ setStep("url");
9449
+ } else {
9450
+ onCancel?.();
9451
+ }
9452
+ });
9353
9453
  const submitKey = async (value) => {
9354
9454
  if (value.trim() === "") return;
9355
9455
  setStep("working");
@@ -10355,13 +10455,12 @@ function ContextMeter({
10355
10455
  }
10356
10456
 
10357
10457
  // src/ui/components/SubagentChat.tsx
10358
- import { Box as Box12, Text as Text12, useInput as useInput2 } from "ink";
10458
+ import { Box as Box12, Text as Text12, useInput as useInput3 } from "ink";
10359
10459
  import { useState as useState7 } from "react";
10360
10460
 
10361
10461
  // src/ui/components/Transcript.tsx
10362
10462
  import { Box as Box11, Static, Text as Text11 } from "ink";
10363
10463
  import { memo as memo2, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef4, useState as useState6 } from "react";
10364
- import Spinner2 from "ink-spinner";
10365
10464
  import stringWidth4 from "string-width";
10366
10465
 
10367
10466
  // src/ui/markdown.tsx
@@ -11251,10 +11350,7 @@ function AssistantView({ bubble: source, maxRows }) {
11251
11350
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, children: [
11252
11351
  frameThinking !== "" && /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx11(Markdown, { dim: true, children: frameThinking }) }),
11253
11352
  bubble.streaming ? /* @__PURE__ */ jsx11(Text11, { children: frameText }) : /* @__PURE__ */ jsx11(Markdown, { children: bubble.text }),
11254
- bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
11255
- /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
11256
- " thinking"
11257
- ] })
11353
+ bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "thinking\u2026" })
11258
11354
  ] });
11259
11355
  }
11260
11356
  function clipTextToRows(text, maxRows, columns) {
@@ -11415,7 +11511,7 @@ function SubagentChat({
11415
11511
  const strings = useStrings();
11416
11512
  const [offset, setOffset] = useState7(0);
11417
11513
  const total = bubble?.bubbles.length ?? 0;
11418
- useInput2((_input, key) => {
11514
+ useInput3((_input, key) => {
11419
11515
  if (key.escape) onClose();
11420
11516
  else if (key.upArrow) setOffset((value) => Math.min(Math.max(0, total - 1), value + 1));
11421
11517
  else if (key.downArrow) setOffset((value) => Math.max(0, value - 1));
@@ -11464,25 +11560,44 @@ function SubagentChat({
11464
11560
 
11465
11561
  // src/ui/components/TerminalViewport.tsx
11466
11562
  import { Box as Box13 } from "ink";
11467
- import { jsx as jsx13 } from "react/jsx-runtime";
11468
- var INTERACTIVE_CHROME_ROWS = 11;
11563
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
11564
+ var CHROME_ROWS = 16;
11469
11565
  var MAX_LIVE_ROWS = 11;
11470
11566
  var MIN_LIVE_ROWS = 4;
11471
- function liveTranscriptRows(rows) {
11567
+ var BOTTOM_SLACK_ROWS = 3;
11568
+ function liveTranscriptRows(rows, chromeRows) {
11472
11569
  if (!Number.isFinite(rows)) return MAX_LIVE_ROWS;
11473
- return Math.max(MIN_LIVE_ROWS, Math.min(MAX_LIVE_ROWS, Math.floor(rows) - INTERACTIVE_CHROME_ROWS - 2));
11570
+ const chrome = chromeRows ?? CHROME_ROWS;
11571
+ const budget = Math.floor(rows) - chrome - BOTTOM_SLACK_ROWS - 1;
11572
+ return Math.max(MIN_LIVE_ROWS, Math.min(MAX_LIVE_ROWS, budget));
11474
11573
  }
11475
11574
  function TerminalViewport({ children, rows }) {
11476
11575
  const { columns } = useTerminalSize();
11477
- return /* @__PURE__ */ jsx13(
11576
+ const liveRows = liveTranscriptRows(rows);
11577
+ const items = Array.isArray(children) ? children : [children];
11578
+ const [transcript, ...chromeChildren] = items;
11579
+ return /* @__PURE__ */ jsxs13(
11478
11580
  Box13,
11479
11581
  {
11480
- width: columns,
11582
+ width: columns - 1,
11481
11583
  maxWidth: "100%",
11482
11584
  flexDirection: "column",
11483
- maxHeight: liveTranscriptRows(rows) + INTERACTIVE_CHROME_ROWS,
11585
+ maxHeight: liveRows + CHROME_ROWS,
11484
11586
  overflowY: "hidden",
11485
- children
11587
+ children: [
11588
+ /* @__PURE__ */ jsx13(
11589
+ Box13,
11590
+ {
11591
+ flexDirection: "column",
11592
+ flexShrink: 1,
11593
+ minHeight: 0,
11594
+ maxHeight: liveRows,
11595
+ overflowY: "hidden",
11596
+ children: transcript
11597
+ }
11598
+ ),
11599
+ /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", flexShrink: 0, children: chromeChildren })
11600
+ ]
11486
11601
  }
11487
11602
  );
11488
11603
  }
@@ -11740,7 +11855,7 @@ function safeSummary(summarize, name, input) {
11740
11855
  }
11741
11856
 
11742
11857
  // src/ui/App.tsx
11743
- import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
11858
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
11744
11859
  var EFFORTS = ["auto", "low", "medium", "high", "xhigh", "max"];
11745
11860
  var STREAM_FRAME_MS = 50;
11746
11861
  var CUSTOM_ACCENT = "__custom__";
@@ -12022,6 +12137,7 @@ function App({
12022
12137
  });
12023
12138
  sessionStart.current = Date.now();
12024
12139
  turns.current = 0;
12140
+ speedMeter.current = createSpeedMeter();
12025
12141
  void runtime.persist([]).catch(
12026
12142
  (error) => notice("error", error instanceof Error ? error.message : String(error))
12027
12143
  );
@@ -12758,6 +12874,68 @@ ${strings.mcpAddUsage}`
12758
12874
  notice("info", runtime.readMemory() || strings.memoryEmpty);
12759
12875
  return;
12760
12876
  }
12877
+ case "systemp": {
12878
+ const sub = rest[0]?.toLowerCase();
12879
+ if (sub === "set") {
12880
+ const text = rawRest.replace(/^\S+\s*/, "").trim();
12881
+ if (!text) {
12882
+ const typed = await promptInput(strings.systemPromptTitle);
12883
+ if (!typed || typed.trim() === "") return;
12884
+ await runtime.setSystemPrompt(typed);
12885
+ notice("info", strings.systemPromptSet);
12886
+ return;
12887
+ }
12888
+ await runtime.setSystemPrompt(text);
12889
+ notice("info", strings.systemPromptSet);
12890
+ return;
12891
+ }
12892
+ if (sub === "load") {
12893
+ const filePath = rawRest.replace(/^\S+\s*/, "").trim();
12894
+ if (!filePath) {
12895
+ notice("warn", strings.systemPromptUsage);
12896
+ return;
12897
+ }
12898
+ try {
12899
+ const text = await runtime.loadSystemPromptFile(filePath);
12900
+ await runtime.setSystemPrompt(text);
12901
+ notice("info", strings.systemPromptSet);
12902
+ } catch (error) {
12903
+ notice("error", error instanceof Error ? error.message : String(error));
12904
+ }
12905
+ return;
12906
+ }
12907
+ if (sub === "clear") {
12908
+ if (!runtime.getSystemPrompt()) {
12909
+ notice("info", strings.systemPromptEmpty);
12910
+ return;
12911
+ }
12912
+ await runtime.setSystemPrompt("");
12913
+ notice("info", strings.systemPromptCleared);
12914
+ return;
12915
+ }
12916
+ if (sub === "on") {
12917
+ await runtime.setSystemPromptEnabled(true);
12918
+ notice("info", strings.systemPromptEnabled);
12919
+ return;
12920
+ }
12921
+ if (sub === "off") {
12922
+ await runtime.setSystemPromptEnabled(false);
12923
+ notice("info", strings.systemPromptDisabled);
12924
+ return;
12925
+ }
12926
+ if (sub !== void 0 && sub !== "" && sub !== "show") {
12927
+ notice("warn", strings.systemPromptUsage);
12928
+ return;
12929
+ }
12930
+ const current = runtime.getSystemPrompt();
12931
+ if (!current) {
12932
+ notice("info", strings.systemPromptEmpty);
12933
+ return;
12934
+ }
12935
+ const enabled = runtime.isSystemPromptEnabled();
12936
+ notice("info", (enabled ? "" : "[disabled] ") + current);
12937
+ return;
12938
+ }
12761
12939
  case "effort": {
12762
12940
  notice("info", runtime.effortDescription(history.current));
12763
12941
  const choice = EFFORTS.includes(rawRest) ? rawRest : await pick(
@@ -12770,6 +12948,21 @@ ${strings.mcpAddUsage}`
12770
12948
  forceRender((n) => n + 1);
12771
12949
  return;
12772
12950
  }
12951
+ case "steps": {
12952
+ const current = runtime.getMaxTurnSteps();
12953
+ if (!rest[0]) {
12954
+ notice("info", strings.stepsCurrent(current));
12955
+ return;
12956
+ }
12957
+ const parsed = Number(rest[0]);
12958
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1e3) {
12959
+ notice("warn", strings.stepsInvalid);
12960
+ return;
12961
+ }
12962
+ await runtime.setMaxTurnSteps(parsed);
12963
+ notice("info", parsed === 0 ? strings.stepsUnlimited : strings.stepsSet(parsed));
12964
+ return;
12965
+ }
12773
12966
  case "budget": {
12774
12967
  const current = runtime.getMaxTokensPerTurn();
12775
12968
  if (rest[0]) {
@@ -13259,13 +13452,16 @@ ${strings.mcpAddUsage}`
13259
13452
  notice("info", `${summary2}
13260
13453
  ${strings.configAt(runtime.configPath())}`);
13261
13454
  return summary2;
13455
+ },
13456
+ onCancel: () => {
13457
+ if (!runtime.needsSetup()) setSetup(false);
13262
13458
  }
13263
13459
  }
13264
13460
  )
13265
13461
  );
13266
13462
  }
13267
13463
  return shell(
13268
- /* @__PURE__ */ jsxs13(TerminalViewport, { rows, children: [
13464
+ /* @__PURE__ */ jsxs14(TerminalViewport, { rows, children: [
13269
13465
  /* @__PURE__ */ jsx14(
13270
13466
  Transcript,
13271
13467
  {
@@ -13395,10 +13591,10 @@ function TextInputOverlay({
13395
13591
  const submit = useCallback(() => {
13396
13592
  onAnswer(value || null);
13397
13593
  }, [value, onAnswer]);
13398
- useInput3((_input, key) => {
13594
+ useInput4((_input, key) => {
13399
13595
  if (key.escape) onCancel();
13400
13596
  });
13401
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", children: [
13597
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
13402
13598
  /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: title }),
13403
13599
  /* @__PURE__ */ jsx14(
13404
13600
  TextInput2,
@@ -13518,10 +13714,15 @@ function clearTerminal() {
13518
13714
  if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
13519
13715
  }
13520
13716
  var TUI_RENDER_OPTIONS = {
13521
- // Keep unchanged streamed lines in place instead of clearing and redrawing
13522
- // the whole live region on every frame.
13523
- incrementalRendering: true,
13524
- maxFps: 30
13717
+ // Incremental rendering is off: Ink's incremental diff races with its own
13718
+ // <Static> commits and leaves stale rows behind (the prompt frame appears
13719
+ // twice). Standard rendering clears the whole frame each time — a little
13720
+ // more work, but no stale lines.
13721
+ incrementalRendering: false,
13722
+ // Throttling defers writes; when a bubble closes into <Static> mid-stream,
13723
+ // the deferred frame and the static write race and leave stale rows behind.
13724
+ // Unthrottled renders cost a little CPU but keep every frame consistent.
13725
+ maxFps: 0
13525
13726
  };
13526
13727
  async function startTui(options) {
13527
13728
  const { runtime, history, warnings, shutdown } = await boot({