neoctl 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/agents/local-agent-task.js +1 -2
  2. package/dist/agents/local-agent-task.js.map +1 -1
  3. package/dist/agents/smoke-agents.js +4 -21
  4. package/dist/agents/smoke-agents.js.map +1 -1
  5. package/dist/context/prompts.js +0 -4
  6. package/dist/context/prompts.js.map +1 -1
  7. package/dist/core/query-engine.d.ts +1 -22
  8. package/dist/core/query-engine.js +12 -106
  9. package/dist/core/query-engine.js.map +1 -1
  10. package/dist/core/query.d.ts +1 -2
  11. package/dist/core/query.js +5 -59
  12. package/dist/core/query.js.map +1 -1
  13. package/dist/core/smoke-core-loop.js +6 -95
  14. package/dist/core/smoke-core-loop.js.map +1 -1
  15. package/dist/index.d.ts +1 -25
  16. package/dist/index.js +1 -25
  17. package/dist/index.js.map +1 -1
  18. package/dist/model/communication-logger.d.ts +1 -2
  19. package/dist/model/communication-logger.js +0 -3
  20. package/dist/model/communication-logger.js.map +1 -1
  21. package/dist/model/config.d.ts +4 -10
  22. package/dist/model/config.js +12 -61
  23. package/dist/model/config.js.map +1 -1
  24. package/dist/model/context-window.js +0 -1
  25. package/dist/model/context-window.js.map +1 -1
  26. package/dist/model/env.js +19 -35
  27. package/dist/model/env.js.map +1 -1
  28. package/dist/model/model-metadata.json +677 -726
  29. package/dist/model/openai-adapter.d.ts +1 -1
  30. package/dist/model/openai-chat-mapper.d.ts +1 -4
  31. package/dist/model/openai-chat-mapper.js +8 -30
  32. package/dist/model/openai-chat-mapper.js.map +1 -1
  33. package/dist/model/openai-mappers.d.ts +2 -5
  34. package/dist/model/openai-mappers.js +4 -17
  35. package/dist/model/openai-mappers.js.map +1 -1
  36. package/dist/model/openai-responses-mapper.d.ts +1 -1
  37. package/dist/model/openai-responses-mapper.js +1 -2
  38. package/dist/model/openai-responses-mapper.js.map +1 -1
  39. package/dist/model/provider-factory.js +0 -32
  40. package/dist/model/provider-factory.js.map +1 -1
  41. package/dist/model/smoke-openai.js +1 -1
  42. package/dist/model/smoke-openai.js.map +1 -1
  43. package/dist/model/smoke-responses-mapper.js +6 -6
  44. package/dist/model/smoke-responses-mapper.js.map +1 -1
  45. package/dist/repl/commands.d.ts +0 -15
  46. package/dist/repl/commands.js +0 -58
  47. package/dist/repl/commands.js.map +1 -1
  48. package/dist/repl/index.js +144 -970
  49. package/dist/repl/index.js.map +1 -1
  50. package/dist/repl/render.js +2 -0
  51. package/dist/repl/render.js.map +1 -1
  52. package/dist/repl/status-line.d.ts +1 -0
  53. package/dist/repl/status-line.js +34 -27
  54. package/dist/repl/status-line.js.map +1 -1
  55. package/dist/session/session-store.js +2 -2
  56. package/dist/session/session-store.js.map +1 -1
  57. package/dist/session/smoke-session.js +1 -22
  58. package/dist/session/smoke-session.js.map +1 -1
  59. package/dist/skills/skill-tool.d.ts +5 -85
  60. package/dist/skills/skill-tool.js +14 -173
  61. package/dist/skills/skill-tool.js.map +1 -1
  62. package/dist/skills/smoke-skills.js +5 -54
  63. package/dist/skills/smoke-skills.js.map +1 -1
  64. package/dist/tools/builtins/search-providers.d.ts +1 -15
  65. package/dist/tools/builtins/search-providers.js +1 -195
  66. package/dist/tools/builtins/search-providers.js.map +1 -1
  67. package/dist/tools/builtins/search-tool.js +2 -2
  68. package/dist/tools/builtins/search-tool.js.map +1 -1
  69. package/dist/tools/registry.d.ts +0 -1
  70. package/dist/tools/registry.js +0 -11
  71. package/dist/tools/registry.js.map +1 -1
  72. package/dist/tools/run-tool-use.js +1 -1
  73. package/dist/tools/run-tool-use.js.map +1 -1
  74. package/dist/tools/smoke-tool-system.js +9 -43
  75. package/dist/tools/smoke-tool-system.js.map +1 -1
  76. package/dist/tools/tool.d.ts +1 -9
  77. package/dist/tools/tool.js.map +1 -1
  78. package/package.json +1 -1
  79. package/scripts/build-standalone.mjs +139 -139
  80. package/dist/model/deepseek-adapter.d.ts +0 -29
  81. package/dist/model/deepseek-adapter.js +0 -108
  82. package/dist/model/deepseek-adapter.js.map +0 -1
  83. package/dist/model/kimi-adapter.d.ts +0 -29
  84. package/dist/model/kimi-adapter.js +0 -108
  85. package/dist/model/kimi-adapter.js.map +0 -1
  86. package/dist/model/smoke-deepseek-mapper.d.ts +0 -1
  87. package/dist/model/smoke-deepseek-mapper.js +0 -65
  88. package/dist/model/smoke-deepseek-mapper.js.map +0 -1
  89. package/dist/open-directory.d.ts +0 -1
  90. package/dist/open-directory.js +0 -26
  91. package/dist/open-directory.js.map +0 -1
  92. package/dist/paths.d.ts +0 -7
  93. package/dist/paths.js +0 -12
  94. package/dist/paths.js.map +0 -1
  95. package/dist/session/session-export.d.ts +0 -33
  96. package/dist/session/session-export.js +0 -351
  97. package/dist/session/session-export.js.map +0 -1
  98. package/dist/session/simple-session-runtime.d.ts +0 -74
  99. package/dist/session/simple-session-runtime.js +0 -171
  100. package/dist/session/simple-session-runtime.js.map +0 -1
  101. package/dist/skills/skill-filesystem.d.ts +0 -32
  102. package/dist/skills/skill-filesystem.js +0 -371
  103. package/dist/skills/skill-filesystem.js.map +0 -1
  104. package/dist/skills/skill-management-tools.d.ts +0 -36
  105. package/dist/skills/skill-management-tools.js +0 -188
  106. package/dist/skills/skill-management-tools.js.map +0 -1
  107. package/dist/tips.d.ts +0 -10
  108. package/dist/tips.js +0 -168
  109. package/dist/tips.js.map +0 -1
  110. package/dist/tools/builtins/image-generation-tool.d.ts +0 -68
  111. package/dist/tools/builtins/image-generation-tool.js +0 -315
  112. package/dist/tools/builtins/image-generation-tool.js.map +0 -1
  113. package/dist/ui/display-message.d.ts +0 -101
  114. package/dist/ui/display-message.js +0 -113
  115. package/dist/ui/display-message.js.map +0 -1
  116. package/dist/web/html.d.ts +0 -1
  117. package/dist/web/html.js +0 -858
  118. package/dist/web/html.js.map +0 -1
  119. package/dist/web/index.d.ts +0 -2
  120. package/dist/web/index.js +0 -1810
  121. package/dist/web/index.js.map +0 -1
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs/promises";
3
- import { existsSync, readFileSync } from "node:fs";
4
3
  import path from "node:path";
5
4
  import { stdin, stdout } from "node:process";
6
5
  import React, { useCallback, useEffect, useRef, useState } from "react";
@@ -8,29 +7,25 @@ import { Box, Static, Text, render, useApp, useInput } from "ink";
8
7
  import stripAnsi from "strip-ansi";
9
8
  import wrapAnsi from "wrap-ansi";
10
9
  import { QueryEngine } from "../core/query-engine.js";
11
- import { getUserDotEnvPath, loadDefaultDotEnvFiles } from "../model/env.js";
10
+ import { loadDefaultDotEnvFiles } from "../model/env.js";
12
11
  import { readModelProviderConfig } from "../model/config.js";
13
- import { findModelMetadata, loadModelCatalog, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
12
+ import { loadModelCatalog, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
14
13
  import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
15
- import { createModelGatewayFromConfig, createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
14
+ import { createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
16
15
  import { ToolRegistry } from "../tools/registry.js";
16
+ import { echoTool } from "../tools/builtins/echo-tool.js";
17
17
  import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
18
18
  import { createExecTool } from "../tools/builtins/exec-tool.js";
19
19
  import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
20
20
  import { grepTool } from "../tools/builtins/grep-tool.js";
21
21
  import { searchTool } from "../tools/builtins/search-tool.js";
22
22
  import { planTool } from "../tools/builtins/plan-tool.js";
23
- import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
24
23
  import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
25
24
  import { createTaskTools } from "../tasks/task-tools.js";
26
25
  import { TaskStore } from "../tasks/task-store.js";
27
- import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
26
+ import { isModelReasoningArgument, isValidReplCommandLine, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
28
27
  import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
29
- import { writeSessionMarkdownExport } from "../session/session-export.js";
30
28
  import { readClipboard } from "./clipboard.js";
31
- import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
32
- import { openDirectory } from "../open-directory.js";
33
- import { runWebServer } from "../web/index.js";
34
29
  const e = React.createElement;
35
30
  class SessionUsageTracker {
36
31
  totals = emptyUsageTotals();
@@ -88,45 +83,14 @@ function sumUsageTokens(left, right) {
88
83
  return undefined;
89
84
  return (left ?? 0) + (right ?? 0);
90
85
  }
91
- async function main(argv = process.argv.slice(2)) {
92
- const webArgs = parseWebCliArgs(argv);
93
- if (webArgs) {
94
- await runWebServer(webArgs);
95
- return;
96
- }
97
- const initialCommand = parseCliReplCommandArgs(argv);
98
- if (argv.length > 0 && !initialCommand) {
99
- console.error(`Unknown or incomplete command: ${argv.join(" ")}\n\n${cliHelpText(binaryName())}`);
100
- process.exitCode = 1;
101
- return;
102
- }
103
- if (initialCommand?.definition.name === "/help") {
104
- console.log(cliHelpText(binaryName()));
105
- return;
106
- }
86
+ async function main() {
107
87
  const runtime = await createRuntime();
108
- const instance = render(e(InkRepl, { runtime, initialCommandLine: initialCommand?.line }), {
88
+ const instance = render(e(InkRepl, { runtime }), {
109
89
  exitOnCtrlC: false,
110
90
  });
111
91
  await instance.waitUntilExit();
112
92
  console.log("bye.");
113
93
  }
114
- function parseWebCliArgs(argv) {
115
- if (argv.length === 0)
116
- return undefined;
117
- const first = argv[0];
118
- if (first !== "-web" && first !== "--web")
119
- return undefined;
120
- return argv.slice(1);
121
- }
122
- function binaryName() {
123
- const arg = process.argv[1];
124
- if (!arg)
125
- return "neo";
126
- const parsed = path.parse(arg);
127
- const name = parsed.name || "neo";
128
- return name === "index" ? "neo" : name;
129
- }
130
94
  function createTaskNotificationSource(taskStore) {
131
95
  return {
132
96
  collectUnnotifiedCompletions() {
@@ -150,6 +114,7 @@ async function createRuntime() {
150
114
  const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
151
115
  const taskStore = new TaskStore();
152
116
  const tools = new ToolRegistry();
117
+ tools.register(echoTool);
153
118
  tools.register(editTool);
154
119
  tools.register(writeTool);
155
120
  tools.register(createExecTool({ taskStore }));
@@ -157,8 +122,6 @@ async function createRuntime() {
157
122
  tools.register(readFileTool);
158
123
  tools.register(grepTool);
159
124
  tools.register(searchTool);
160
- if (modelConfig?.provider === "openai")
161
- tools.register(createOpenAIImageGenerationTool());
162
125
  tools.register(planTool);
163
126
  const agentRuntime = { modelGateway, tools, taskStore };
164
127
  tools.register(createAgentTool(agentRuntime));
@@ -182,7 +145,6 @@ async function createRuntime() {
182
145
  modelGateway,
183
146
  tools,
184
147
  taskNotificationSource,
185
- commands: replCommandDefinitions.map((command) => command.usage),
186
148
  session: {
187
149
  enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
188
150
  sessionId: process.env.AGENT_SESSION_ID,
@@ -194,59 +156,65 @@ async function createRuntime() {
194
156
  },
195
157
  });
196
158
  await engine.initialize();
197
- const initialMetrics = await engine.contextMetrics();
198
159
  return {
199
160
  engine,
200
161
  communicationLogger,
201
- modelGateway,
202
- agentRuntime,
203
162
  usage: new SessionUsageTracker(),
204
163
  taskStore,
205
- tools,
206
- initialMetrics,
164
+ initialMetrics: initialContextMetrics(modelConfig?.model, engine.snapshot().messages, tools.names().length),
207
165
  defaultReasoning: modelConfig?.defaultReasoning,
208
- envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
209
166
  envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
210
167
  };
211
168
  }
212
- function syncImageGenerationTool(runtime, provider) {
213
- runtime.tools.unregister("image2");
214
- if (provider === "openai")
215
- runtime.tools.register(createOpenAIImageGenerationTool());
216
- }
217
169
  function formatCreatedEnvNotice(path) {
218
- return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY or KIMI_API_KEY), then restart neo.`;
170
+ return `Created default config file: ${path}\nFill MODEL_API_KEY in that file, then restart neo.`;
219
171
  }
220
172
  function parseResumeFlag(value) {
221
173
  if (!value)
222
174
  return false;
223
175
  return ["1", "true", "yes", "latest"].includes(value.toLowerCase());
224
176
  }
225
- function activeBackgroundTasks(runtime) {
226
- return runtime.taskStore.list().filter((task) => !runtime.taskStore.isTerminal(task));
227
- }
228
- function runningSessionIds(runs) {
229
- return [...runs.keys()];
177
+ function initialContextMetrics(model, messageCount, toolCount) {
178
+ const window = resolveContextWindowTokens(model);
179
+ return {
180
+ model,
181
+ estimatedInputTokens: 0,
182
+ estimatedChars: 0,
183
+ messageCount,
184
+ toolCount,
185
+ contextWindowTokens: window.tokens,
186
+ contextWindowSource: window.source,
187
+ contextUsageRatio: window.tokens ? 0 : undefined,
188
+ modelMetadata: window.model
189
+ ? {
190
+ id: window.model.id,
191
+ provider: window.model.provider,
192
+ maxOutputTokens: window.model.maxOutputTokens,
193
+ knowledgeCutoff: window.model.knowledgeCutoff,
194
+ reasoning: window.model.reasoning,
195
+ imageInput: window.model.imageInput,
196
+ source: window.model.source,
197
+ }
198
+ : undefined,
199
+ };
230
200
  }
231
- function initialStatus(runtime, metrics = runtime.initialMetrics) {
201
+ function initialStatus(runtime) {
232
202
  return {
233
203
  phase: "ready",
234
204
  metrics: {
235
- ...metrics,
205
+ ...runtime.initialMetrics,
236
206
  messageCount: runtime.engine.snapshot().messages,
237
207
  },
238
208
  streamedOutputTokens: 0,
239
209
  activityTick: 0,
240
210
  };
241
211
  }
242
- async function resetStatus(runtime) {
243
- return initialStatus(runtime, await runtime.engine.contextMetrics());
244
- }
245
- function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
212
+ function setTerminalTitle(title, dotFilled = true) {
246
213
  if (!stdout.isTTY)
247
214
  return;
248
215
  const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
249
- const decoratedTitle = `${prefix}${safeTitle || "neo"}`.slice(0, 120);
216
+ const dotPrefix = dotFilled ? TERMINAL_TITLE_DOT_FILLED_PREFIX : TERMINAL_TITLE_DOT_BLANK_PREFIX;
217
+ const decoratedTitle = `${dotPrefix}${safeTitle || "neo"}`.slice(0, 120);
250
218
  stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
251
219
  }
252
220
  function playReadySound() {
@@ -369,7 +337,7 @@ function pushTextBlock(blocks, text) {
369
337
  function escapeRegExp(value) {
370
338
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
371
339
  }
372
- function InkRepl({ runtime, initialCommandLine }) {
340
+ function InkRepl({ runtime }) {
373
341
  const app = useApp();
374
342
  const lineId = useRef(0);
375
343
  const assistantLineId = useRef(undefined);
@@ -386,19 +354,13 @@ function InkRepl({ runtime, initialCommandLine }) {
386
354
  const queuedAttachmentsRef = useRef(undefined);
387
355
  const [cursor, setCursor] = useState(0);
388
356
  const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
389
- const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
390
357
  const [busy, setBusy] = useState(false);
391
358
  const [status, setStatus] = useState(() => initialStatus(runtime));
392
359
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
393
- const [backgroundTasks, setBackgroundTasks] = useState(() => activeBackgroundTasks(runtime));
394
- const [backgroundSessionRuns, setBackgroundSessionRuns] = useState([]);
395
- const backgroundSessionRunsRef = useRef(new Map());
396
- const suppressReattachedStreamingRef = useRef(new Set());
397
- const activePromptRunRef = useRef(undefined);
360
+ const [backgroundTaskCount, setBackgroundTaskCount] = useState(() => runtime.taskStore.activeCount());
398
361
  const [animationTick, setAnimationTick] = useState(0);
399
- const [terminalTitlePrefix, setTerminalTitlePrefix] = useState(TERMINAL_TITLE_READY_PREFIX);
400
- const backgroundTaskCount = backgroundTasks.length;
401
- const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0 || backgroundSessionRuns.length > 0;
362
+ const [terminalTitleDotVisible, setTerminalTitleDotVisible] = useState(true);
363
+ const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0;
402
364
  const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
403
365
  const inputRef = useRef(input);
404
366
  const queuedInputRef = useRef(undefined);
@@ -415,8 +377,6 @@ function InkRepl({ runtime, initialCommandLine }) {
415
377
  const [pasteStatus, setPasteStatus] = useState(undefined);
416
378
  const pasteStatusTimerRef = useRef(undefined);
417
379
  const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
418
- const [loginForm, setLoginForm] = useState(undefined);
419
- const loginFormRef = useRef(undefined);
420
380
  useEffect(() => {
421
381
  enableTerminalFocusReporting();
422
382
  enableTerminalMouseReporting();
@@ -426,35 +386,36 @@ function InkRepl({ runtime, initialCommandLine }) {
426
386
  };
427
387
  }, []);
428
388
  useEffect(() => {
429
- if (!busy && backgroundTaskCount === 0 && backgroundSessionRuns.length === 0)
389
+ if (!busy && backgroundTaskCount === 0)
430
390
  return undefined;
431
391
  const interval = setInterval(() => setAnimationTick((current) => current + 1), REPL_ANIMATION_INTERVAL_MS);
432
392
  return () => clearInterval(interval);
433
- }, [busy, backgroundTaskCount, backgroundSessionRuns.length]);
393
+ }, [busy, backgroundTaskCount]);
434
394
  useEffect(() => {
435
- const updateBackgroundTasks = () => setBackgroundTasks(activeBackgroundTasks(runtime));
436
- updateBackgroundTasks();
437
- return runtime.taskStore.subscribe(updateBackgroundTasks);
395
+ const updateBackgroundTaskCount = () => setBackgroundTaskCount(runtime.taskStore.activeCount());
396
+ updateBackgroundTaskCount();
397
+ return runtime.taskStore.subscribe(updateBackgroundTaskCount);
438
398
  }, [runtime]);
439
399
  useEffect(() => {
440
400
  if (!terminalTitleWorking) {
441
- setTerminalTitlePrefix(TERMINAL_TITLE_READY_PREFIX);
401
+ setTerminalTitleDotVisible(true);
442
402
  return undefined;
443
403
  }
444
- setTerminalTitlePrefix(TERMINAL_TITLE_WORKING_PREFIX);
445
- return undefined;
404
+ setTerminalTitleDotVisible(true);
405
+ const interval = setInterval(() => setTerminalTitleDotVisible((visible) => !visible), TERMINAL_TITLE_BLINK_INTERVAL_MS);
406
+ return () => clearInterval(interval);
446
407
  }, [terminalTitleWorking]);
447
408
  useEffect(() => {
448
409
  const updateTitle = (snapshot) => {
449
410
  sessionTitleRef.current = sessionTerminalTitle(snapshot);
450
- setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
411
+ setTerminalTitle(sessionTitleRef.current, terminalTitleDotVisible);
451
412
  };
452
413
  updateTitle(runtime.engine.snapshot().session);
453
414
  return runtime.engine.onSessionTitleChange(updateTitle);
454
- }, [runtime, terminalTitlePrefix]);
415
+ }, [runtime, terminalTitleDotVisible]);
455
416
  useEffect(() => {
456
- setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
457
- }, [terminalTitlePrefix]);
417
+ setTerminalTitle(sessionTitleRef.current, terminalTitleDotVisible);
418
+ }, [terminalTitleDotVisible]);
458
419
  const setPromptState = (text, nextCursor, options) => {
459
420
  const safeCursor = Math.max(0, Math.min(nextCursor, text.length));
460
421
  inputRef.current = text;
@@ -481,10 +442,6 @@ function InkRepl({ runtime, initialCommandLine }) {
481
442
  setSlashCompletionIndex(safeIndex);
482
443
  };
483
444
  const resetSlashCompletionSelection = () => setSlashCompletionSelection(0);
484
- const setLoginFormState = (next) => {
485
- loginFormRef.current = next;
486
- setLoginForm(next);
487
- };
488
445
  const syncAttachmentsForText = (text) => {
489
446
  const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
490
447
  if (next.length === attachmentsRef.current.length)
@@ -511,11 +468,9 @@ function InkRepl({ runtime, initialCommandLine }) {
511
468
  }, PASTE_STATUS_DISPLAY_MS);
512
469
  pasteStatusTimerRef.current = timer;
513
470
  };
514
- const advanceTip = () => setTipIndex((current) => current + 1);
515
471
  const insertAtCursor = (value) => {
516
472
  const currentText = inputRef.current;
517
473
  const currentCursor = cursorRef.current;
518
- advanceTip();
519
474
  setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
520
475
  };
521
476
  const insertAttachmentLabel = (attachment) => {
@@ -531,10 +486,6 @@ function InkRepl({ runtime, initialCommandLine }) {
531
486
  return;
532
487
  }
533
488
  if (payload.type === "image") {
534
- if (!runtime.engine.canAcceptImageInput()) {
535
- setPasteStatusMessage("current model does not support image input; image was not added");
536
- return;
537
- }
538
489
  const id = ++imageAttachmentCounterRef.current;
539
490
  insertAttachmentLabel({ id, kind: "image", label: `[img#${id}]`, image: payload.image });
540
491
  setPasteStatusMessage(undefined);
@@ -587,73 +538,17 @@ function InkRepl({ runtime, initialCommandLine }) {
587
538
  const replaceLine = (id, patch) => {
588
539
  setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
589
540
  };
590
- const syncBackgroundSessionRuns = () => {
591
- setBackgroundSessionRuns([...backgroundSessionRunsRef.current.values()]);
592
- };
593
- const detachRunningForeground = (reason) => {
594
- if (!busyRef.current)
595
- return false;
596
- const snapshot = runtime.engine.snapshot().session;
597
- const sessionId = snapshot?.sessionId ?? `session-${Date.now().toString(36)}`;
598
- const run = activePromptRunRef.current;
599
- if (run && !backgroundSessionRunsRef.current.has(sessionId)) {
600
- const backgroundRun = {
601
- sessionId,
602
- title: snapshot?.title,
603
- reason,
604
- startedAt: Date.now(),
605
- engine: runtime.engine,
606
- abortController: activeAbortController.current ?? new AbortController(),
607
- promise: run,
608
- };
609
- backgroundSessionRunsRef.current.set(sessionId, backgroundRun);
610
- syncBackgroundSessionRuns();
611
- setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
612
- run.finally(() => {
613
- backgroundSessionRunsRef.current.delete(sessionId);
614
- suppressReattachedStreamingRef.current.delete(backgroundRun.engine);
615
- syncBackgroundSessionRuns();
616
- setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
617
- }).catch(() => undefined);
618
- }
619
- activeAbortController.current = undefined;
620
- interruptArmed.current = false;
621
- setQueuedPromptState(undefined);
622
- setBusyState(false);
623
- setStatus((current) => ({ ...current, phase: "ready", detail: undefined }));
624
- append(systemLine(`Detached running ${sessionId} to background for ${reason}.`));
625
- return true;
626
- };
627
- const resetForegroundView = (metrics) => {
541
+ const resumeSnapshot = (snapshot) => {
628
542
  runtime.usage.reset();
629
- setStatus(initialStatus(runtime, metrics));
543
+ setStatus(initialStatus(runtime));
630
544
  resetLinesToHistory(runtime, setLines, lineId);
631
545
  assistantLineId.current = undefined;
632
546
  thinkingLineId.current = undefined;
633
547
  finalizedThinkingLineId.current = undefined;
634
548
  toolLineIds.current.clear();
635
549
  clearPendingToolResultTimers();
636
- };
637
- const resumeSnapshot = (snapshot, metrics) => {
638
- resetForegroundView(metrics);
639
550
  append(systemLine(formatResume(snapshot)));
640
551
  };
641
- const reattachRunningSession = async (run) => {
642
- detachRunningForeground("session switch");
643
- backgroundSessionRunsRef.current.delete(run.sessionId);
644
- syncBackgroundSessionRuns();
645
- setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
646
- runtime.engine = run.engine;
647
- activeAbortController.current = run.abortController;
648
- interruptArmed.current = false;
649
- activePromptRunRef.current = run.promise;
650
- suppressReattachedStreamingRef.current.add(run.engine);
651
- const metrics = await runtime.engine.contextMetrics();
652
- resetForegroundView(metrics);
653
- setBusyState(true);
654
- setStatus((current) => ({ ...current, phase: "running", detail: "working" }));
655
- append(systemLine(`reattached running session ${run.sessionId}`));
656
- };
657
552
  const finalizeLiveLine = (id) => {
658
553
  if (id === undefined)
659
554
  return;
@@ -798,12 +693,14 @@ function InkRepl({ runtime, initialCommandLine }) {
798
693
  const trimmed = text.trim();
799
694
  if (!trimmed)
800
695
  return;
801
- if (submitAttachments.some((attachment) => attachment.kind === "image") && !runtime.engine.canAcceptImageInput()) {
802
- append({ kind: "error", text: "Current model does not support image input; image attachments were not added to the conversation." });
803
- return;
804
- }
805
696
  if (busyRef.current) {
806
- detachRunningForeground("new prompt");
697
+ if (queuedInputRef.current !== undefined)
698
+ return;
699
+ setQueuedPromptState(text, submitAttachments);
700
+ setHistorySelection(undefined);
701
+ setPromptState("", 0);
702
+ clearAttachments();
703
+ return;
807
704
  }
808
705
  history.current = [text, ...history.current.filter((entry) => entry !== text)].slice(0, 100);
809
706
  setHistorySelection(undefined);
@@ -901,68 +798,16 @@ function InkRepl({ runtime, initialCommandLine }) {
901
798
  if (command.type === "reset") {
902
799
  runtime.engine.reset();
903
800
  runtime.usage.reset();
904
- setStatus(await resetStatus(runtime));
801
+ setStatus(initialStatus(runtime));
905
802
  append(systemLine("transcript reset"));
906
803
  return;
907
804
  }
908
805
  if (command.type === "state") {
909
- const contextMetrics = await runtime.engine.contextMetrics();
910
- append(systemLine(formatReplData({ ...runtime.engine.snapshot(), contextMetrics, communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
911
- return;
912
- }
913
- if (command.type === "export") {
914
- setBusyState(true);
915
- setStatus((current) => ({ ...current, phase: "running", detail: "exporting session", activityTick: current.activityTick + 1 }));
916
- try {
917
- const line = await handleExportCommand(command, runtime);
918
- append(line);
919
- }
920
- catch (error) {
921
- append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
922
- }
923
- finally {
924
- setBusyState(false);
925
- setStatus((current) => ({ ...current, phase: "ready", detail: undefined, activityTick: current.activityTick + 1 }));
926
- }
927
- return;
928
- }
929
- if (command.type === "env") {
930
- const envDirectory = path.dirname(runtime.envPath);
931
- try {
932
- await fs.mkdir(envDirectory, { recursive: true });
933
- await openDirectory(envDirectory);
934
- append({ kind: "system", title: "System", text: `Opened env directory: ${envDirectory}`, format: "plain", previewStyle: "summary" });
935
- }
936
- catch (error) {
937
- append({ kind: "error", text: `Failed to open env directory ${envDirectory}: ${error instanceof Error ? error.message : String(error)}`, format: "plain" });
938
- }
939
- return;
940
- }
941
- if (command.type === "new") {
942
- detachRunningForeground("new session");
943
- runtime.engine = runtime.engine.forkForSession(undefined, false);
944
- await runtime.engine.initialize();
945
- const snapshot = runtime.engine.snapshot().session;
946
- const metrics = await runtime.engine.contextMetrics();
947
- runtime.usage.reset();
948
- setStatus(initialStatus(runtime, metrics));
949
- resetLinesToHistory(runtime, setLines, lineId);
950
- assistantLineId.current = undefined;
951
- thinkingLineId.current = undefined;
952
- finalizedThinkingLineId.current = undefined;
953
- toolLineIds.current.clear();
954
- clearPendingToolResultTimers();
955
- append(systemLine(snapshot ? `new session ${snapshot.sessionId}` : "new session"));
806
+ append(systemLine(formatReplData({ ...runtime.engine.snapshot(), communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
956
807
  return;
957
808
  }
958
809
  if (command.type === "sessions") {
959
- await handleSessionsCommand(runtime, runningSessionIds(backgroundSessionRunsRef.current), setSessionsBrowser, (line) => append(line));
960
- return;
961
- }
962
- if (command.type === "login") {
963
- setSessionsBrowser(undefined);
964
- setLoginFormState(createLoginFormState(runtime.envPath));
965
- append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
810
+ await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
966
811
  return;
967
812
  }
968
813
  if (command.type === "log") {
@@ -970,23 +815,9 @@ function InkRepl({ runtime, initialCommandLine }) {
970
815
  return;
971
816
  }
972
817
  if (command.type === "model") {
973
- setBusyState(true);
974
- setStatus((current) => ({ ...current, phase: "running", detail: "saving model settings", activityTick: current.activityTick + 1 }));
975
- try {
976
- const line = await handleModelCommand(command, runtime);
977
- const metrics = await runtime.engine.contextMetrics();
978
- setStatus((current) => ({
979
- ...current,
980
- phase: "ready",
981
- detail: undefined,
982
- metrics,
983
- activityTick: current.activityTick + 1,
984
- }));
985
- append(line);
986
- }
987
- finally {
988
- setBusyState(false);
989
- }
818
+ const line = handleModelCommand(command, runtime);
819
+ setStatus((current) => ({ ...current, metrics: { ...initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages } }));
820
+ append(line);
990
821
  return;
991
822
  }
992
823
  if (text.trimStart().startsWith("/")) {
@@ -994,10 +825,6 @@ function InkRepl({ runtime, initialCommandLine }) {
994
825
  return;
995
826
  }
996
827
  const promptPayload = buildPromptPayload(command.text, submitAttachments);
997
- if (promptPayload.blocks?.some((block) => block.type === "image") && !runtime.engine.canAcceptImageInput()) {
998
- append({ kind: "error", text: "Current model does not support image input; image attachments were not added to the conversation." });
999
- return;
1000
- }
1001
828
  append({ kind: "user", text });
1002
829
  const abortController = new AbortController();
1003
830
  activeAbortController.current = abortController;
@@ -1013,41 +840,20 @@ function InkRepl({ runtime, initialCommandLine }) {
1013
840
  outputTokenUpdatedAt: undefined,
1014
841
  retryCooldownUntil: undefined,
1015
842
  }));
1016
- const engine = runtime.engine;
1017
- const run = (async () => {
1018
- for await (const event of engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: text })) {
1019
- if (runtime.engine !== engine)
1020
- continue;
1021
- if (suppressReattachedStreamingRef.current.has(engine)) {
1022
- if (event.type === "message" || event.type === "terminal" || event.type === "error" || event.type === "context.metrics" || event.type === "usage") {
1023
- if (event.type === "message" || event.type === "terminal" || event.type === "error")
1024
- suppressReattachedStreamingRef.current.delete(engine);
1025
- handleEvent(event);
1026
- }
1027
- continue;
1028
- }
843
+ try {
844
+ for await (const event of runtime.engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: text })) {
1029
845
  handleEvent(event);
1030
846
  }
1031
- })();
1032
- activePromptRunRef.current = run;
1033
- try {
1034
- await run;
1035
847
  }
1036
848
  catch (error) {
1037
- if (runtime.engine === engine) {
1038
- finalizeLiveLine(assistantLineId.current);
1039
- finalizeThinkingLine();
1040
- finalizeActiveToolLines();
1041
- assistantLineId.current = undefined;
1042
- finalizedThinkingLineId.current = undefined;
1043
- append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
1044
- }
849
+ finalizeLiveLine(assistantLineId.current);
850
+ finalizeThinkingLine();
851
+ finalizeActiveToolLines();
852
+ assistantLineId.current = undefined;
853
+ finalizedThinkingLineId.current = undefined;
854
+ append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
1045
855
  }
1046
856
  finally {
1047
- if (activePromptRunRef.current === run)
1048
- activePromptRunRef.current = undefined;
1049
- if (runtime.engine !== engine)
1050
- return;
1051
857
  if (activeAbortController.current === abortController)
1052
858
  activeAbortController.current = undefined;
1053
859
  interruptArmed.current = false;
@@ -1074,7 +880,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1074
880
  }
1075
881
  };
1076
882
  useEffect(() => {
1077
- setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
1078
883
  setLines(initialLines(runtime, lineId));
1079
884
  assistantLineId.current = undefined;
1080
885
  thinkingLineId.current = undefined;
@@ -1083,26 +888,16 @@ function InkRepl({ runtime, initialCommandLine }) {
1083
888
  clearPendingToolResultTimers();
1084
889
  setStatus(initialStatus(runtime));
1085
890
  setSessionsBrowser(undefined);
1086
- setLoginFormState(undefined);
1087
891
  setQueuedPromptState(undefined);
1088
892
  setPromptState("", 0);
1089
893
  }, [runtime]);
1090
- useEffect(() => {
1091
- if (initialCommandLine === undefined)
1092
- return;
1093
- void submitLine(initialCommandLine);
1094
- }, []);
1095
894
  const terminalSize = useTerminalSize();
1096
895
  const width = terminalSize.columns;
1097
896
  const inputLockedByQueue = busy && queuedInput !== undefined;
1098
897
  const prompt = promptPrefix(busy);
1099
- const currentTip = tipAt(tipIndex);
1100
- const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
1101
- const promptDisplayText = input;
1102
- const promptDisplayCursor = cursor;
1103
- const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
1104
- const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
1105
- const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
898
+ const promptDisplayText = input.length === 0 && promptPlaceholder ? promptPlaceholder : input;
899
+ const promptDisplayCursor = input.length === 0 && promptPlaceholder ? promptPlaceholder.length : cursor;
900
+ const slashCompletions = inputLockedByQueue || promptPlaceholder ? [] : slashCommandCompletions(input, cursor);
1106
901
  const visibleSlashCompletionCount = slashCompletions.length;
1107
902
  const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
1108
903
  ? 0
@@ -1110,7 +905,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1110
905
  if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
1111
906
  slashCompletionIndexRef.current = selectedSlashCompletionIndex;
1112
907
  }
1113
- const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
908
+ const promptHeight = promptTextView(promptDisplayText, promptDisplayCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
1114
909
  const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
1115
910
  const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
1116
911
  const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
@@ -1118,10 +913,9 @@ function InkRepl({ runtime, initialCommandLine }) {
1118
913
  const blockIndex = staticLines.length + i;
1119
914
  return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
1120
915
  }, 0);
1121
- const statusRenderRows = STATUS_BAR_RENDER_ROWS + backgroundTaskStatusRenderRows(backgroundTasks.length);
916
+ const statusRenderRows = STATUS_BAR_RENDER_ROWS + (backgroundTaskCount > 0 ? BACKGROUND_TASK_STATUS_RENDER_ROWS : 0);
1122
917
  const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
1123
- const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
1124
- const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
918
+ const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - dynamicMarginOverhead - 1);
1125
919
  useInput((value, key) => {
1126
920
  if (isTerminalFocusInSequence(value)) {
1127
921
  terminalFocusedRef.current = true;
@@ -1169,10 +963,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1169
963
  restoreQueuedPromptToEditor();
1170
964
  return;
1171
965
  }
1172
- if (loginFormRef.current) {
1173
- handleLoginFormInput(value, key, loginFormRef.current, setLoginFormState, runtime, append, setStatus);
1174
- return;
1175
- }
1176
966
  if (sessionsBrowser) {
1177
967
  if (key.escape) {
1178
968
  setSessionsBrowser(undefined);
@@ -1198,17 +988,10 @@ function InkRepl({ runtime, initialCommandLine }) {
1198
988
  const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
1199
989
  if (selected) {
1200
990
  setSessionsBrowser(undefined);
1201
- const running = backgroundSessionRunsRef.current.get(selected.sessionId);
1202
- if (running) {
1203
- void reattachRunningSession(running);
1204
- }
1205
- else {
1206
- detachRunningForeground("session switch");
1207
- void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
1208
- if (result)
1209
- resumeSnapshot(result.snapshot, result.metrics);
1210
- });
1211
- }
991
+ void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((resumed) => {
992
+ if (resumed)
993
+ resumeSnapshot(resumed);
994
+ });
1212
995
  }
1213
996
  return;
1214
997
  }
@@ -1240,10 +1023,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1240
1023
  if (key.backspace || key.delete) {
1241
1024
  const currentText = inputRef.current;
1242
1025
  const currentCursor = cursorRef.current;
1243
- if (currentText.length === 0) {
1244
- setTipIndex((current) => current + 1);
1245
- return;
1246
- }
1247
1026
  if (currentCursor > 0) {
1248
1027
  setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
1249
1028
  }
@@ -1255,10 +1034,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1255
1034
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1256
1035
  return;
1257
1036
  }
1258
- if (inputRef.current.length === 0) {
1259
- setTipIndex((current) => current - 1);
1260
- return;
1261
- }
1262
1037
  setPromptState(inputRef.current, cursorRef.current - 1);
1263
1038
  return;
1264
1039
  }
@@ -1268,32 +1043,18 @@ function InkRepl({ runtime, initialCommandLine }) {
1268
1043
  setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1269
1044
  return;
1270
1045
  }
1271
- if (inputRef.current.length === 0) {
1272
- setTipIndex((current) => current + 1);
1273
- return;
1274
- }
1275
1046
  setPromptState(inputRef.current, cursorRef.current + 1);
1276
1047
  return;
1277
1048
  }
1278
1049
  if (key.home) {
1279
- if (inputRef.current.length === 0)
1280
- setTipIndex(0);
1281
- else
1282
- setPromptState(inputRef.current, 0);
1050
+ setPromptState(inputRef.current, 0);
1283
1051
  return;
1284
1052
  }
1285
1053
  if (key.end) {
1286
- if (inputRef.current.length === 0)
1287
- setTipIndex((current) => current + 1);
1288
- else
1289
- setPromptState(inputRef.current, inputRef.current.length);
1054
+ setPromptState(inputRef.current, inputRef.current.length);
1290
1055
  return;
1291
1056
  }
1292
1057
  if (key.upArrow) {
1293
- if (inputRef.current.length === 0 && history.current.length === 0) {
1294
- setTipIndex((current) => current - 1);
1295
- return;
1296
- }
1297
1058
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1298
1059
  if (completionCount > 0) {
1299
1060
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
@@ -1307,10 +1068,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1307
1068
  return;
1308
1069
  }
1309
1070
  if (key.downArrow) {
1310
- if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
1311
- setTipIndex((current) => current + 1);
1312
- return;
1313
- }
1314
1071
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1315
1072
  if (completionCount > 0) {
1316
1073
  setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
@@ -1332,10 +1089,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1332
1089
  }
1333
1090
  if (key.tab) {
1334
1091
  const currentText = inputRef.current;
1335
- if (currentText.length === 0) {
1336
- setTipIndex((current) => current + 1);
1337
- return;
1338
- }
1339
1092
  const currentCursor = cursorRef.current;
1340
1093
  const completions = slashCommandCompletions(currentText, currentCursor);
1341
1094
  const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
@@ -1347,10 +1100,9 @@ function InkRepl({ runtime, initialCommandLine }) {
1347
1100
  }
1348
1101
  if (value && !key.ctrl && !key.meta) {
1349
1102
  insertAtCursor(value);
1350
- return;
1351
1103
  }
1352
1104
  });
1353
- return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1105
+ return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTaskCount > 0 ? e(BackgroundTaskStatusLine, { count: backgroundTaskCount, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1354
1106
  }
1355
1107
  const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1356
1108
  const contentWidth = messageContentWidth(width);
@@ -1376,17 +1128,9 @@ function MessageLine({ line, width, contentWidth = messageContentWidth(width), t
1376
1128
  const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
1377
1129
  return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: summaryWidth }, ...renderDisplayText(line, summaryWidth, display.maxLines, display.skipTop)));
1378
1130
  }
1379
- const useRoleMarker = !titleProvidesToolMarker(line);
1380
- const lineWidth = useRoleMarker ? contentWidth : toolWidth;
1381
- const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
1382
- const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1383
- const contentNodes = [];
1384
- if (line.title)
1385
- contentNodes.push(renderBlockTitle(line));
1386
- if (line.bodyTitle)
1387
- contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
1388
- contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
1389
- return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
1131
+ const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, contentWidth);
1132
+ const display = displayWindowForLine(line, contentWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1133
+ return e(Box, { flexDirection: "row" }, e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)), e(Box, { flexDirection: "column", width: contentWidth }, ...renderDisplayText(line, contentWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete)));
1390
1134
  }
1391
1135
  function displayWindowForLine(line, width, maxLines) {
1392
1136
  if (maxLines === undefined)
@@ -1456,21 +1200,12 @@ function summaryTitle(line) {
1456
1200
  function summaryUsesRoleMarker(line) {
1457
1201
  return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
1458
1202
  }
1459
- function titleProvidesToolMarker(line) {
1460
- return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
1461
- }
1462
1203
  function titleStatusMarker(status) {
1463
1204
  return status === "success" ? "✓" : "✗";
1464
1205
  }
1465
1206
  function titleStatusColor(status) {
1466
1207
  return status === "success" ? "green" : "red";
1467
1208
  }
1468
- function renderBlockTitle(line) {
1469
- const title = line.title ?? titleForKind(line.kind);
1470
- if (!line.titleStatus)
1471
- return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
1472
- return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, `${title} `, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, titleStatusMarker(line.titleStatus)));
1473
- }
1474
1209
  function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
1475
1210
  const allPreviewLines = renderSummaryLines(line, width);
1476
1211
  const preview = clipStrings(allPreviewLines, maxLines, skipTop);
@@ -1702,27 +1437,10 @@ function StatusBar({ status, animationTick, width: terminalWidth }) {
1702
1437
  const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
1703
1438
  return e(Box, { marginTop: 1, width, height: 1, overflow: "hidden" }, ...segments.map((segment, index) => e(Text, { key: index, color: segment.color ?? "gray", bold: segment.bold ?? false }, segment.text)));
1704
1439
  }
1705
- function backgroundTaskStatusRenderRows(taskCount) {
1706
- if (taskCount <= 0)
1707
- return 0;
1708
- return 1 + Math.min(taskCount, 2);
1709
- }
1710
- function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
1440
+ function BackgroundTaskStatusLine({ count, width: terminalWidth }) {
1711
1441
  const width = statusBarWidth(terminalWidth);
1712
- const summary = `◇ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
1713
- const detailTasks = tasks.slice(0, 2);
1714
- return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(summary, width)), ...detailTasks.map((task) => e(Text, { key: task.taskId, color: "yellow" }, fitToWidth(` ${task.type}:${truncateMiddle(task.description || task.agentId || task.taskId, Math.max(12, width - 30))} · ${task.status} · ${formatElapsed(Date.now() - new Date(task.createdAt).getTime())}`, width))));
1715
- }
1716
- function formatElapsed(ms) {
1717
- const seconds = Math.max(0, Math.floor(ms / 1000));
1718
- if (seconds < 60)
1719
- return `${seconds}s`;
1720
- const minutes = Math.floor(seconds / 60);
1721
- const remainder = seconds % 60;
1722
- if (minutes < 60)
1723
- return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
1724
- const hours = Math.floor(minutes / 60);
1725
- return `${hours}h${(minutes % 60).toString().padStart(2, "0")}m`;
1442
+ const text = count <= 3 ? "".repeat(Math.max(0, count)) : `◇×${count}`;
1443
+ return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(text, width)));
1726
1444
  }
1727
1445
  function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
1728
1446
  const phase = displayPhase;
@@ -1733,7 +1451,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1733
1451
  const context = renderContextParts(status.metrics);
1734
1452
  const fixedText = [
1735
1453
  phaseText,
1736
- context.percent,
1454
+ `ctx ${context.used} / ${context.limit} (${context.percent})`,
1737
1455
  `↑ ${inputValue}`,
1738
1456
  `↓ ${outputValue}`,
1739
1457
  ].join(STATUS_SEPARATOR);
@@ -1750,7 +1468,9 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1750
1468
  statusDividerSegment(),
1751
1469
  { text: model },
1752
1470
  statusDividerSegment(),
1753
- { text: context.percent, color: contextColor(status.metrics) },
1471
+ statusLabelSegment("ctx"),
1472
+ { text: ` ${context.used} / ${context.limit}` },
1473
+ { text: ` (${context.percent})`, color: contextColor(status.metrics) },
1754
1474
  statusDividerSegment(),
1755
1475
  statusLabelSegment("↑", tokenInputColor),
1756
1476
  { text: ` ${inputValue}` },
@@ -1896,16 +1616,10 @@ function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
1896
1616
  return undefined;
1897
1617
  return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
1898
1618
  }
1899
- function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
1900
- const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
1901
- const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
1902
- const visualLines = promptTextView(displayText, displayCursor, width, prompt);
1619
+ function PromptLine({ text, cursor, busy, locked, placeholder = false, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
1620
+ const visualLines = promptTextView(text, cursor, width, prompt);
1903
1621
  const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
1904
- return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
1905
- const isGhostLine = text.length === 0 && ghostText !== undefined;
1906
- const afterColor = isGhostLine ? "gray" : inputColor;
1907
- return e(Box, { key: `prompt-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: locked ? "gray" : "cyan" }, index === 0 ? prompt : " ".repeat(prompt.length)), ...renderPromptPart(line.before, inputColor, attachments, `prompt-${index}-before`), e(Text, { key: `prompt-${index}-cursor`, inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, afterColor, attachments, `prompt-${index}-after`));
1908
- }), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
1622
+ return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => e(Box, { key: `prompt-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: locked ? "gray" : "cyan" }, index === 0 ? prompt : " ".repeat(prompt.length)), ...renderPromptPart(line.before, inputColor, attachments, `prompt-${index}-before`), e(Text, { key: `prompt-${index}-cursor`, inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, inputColor, attachments, `prompt-${index}-after`))), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
1909
1623
  }
1910
1624
  function PasteStatusLine({ text, width: terminalWidth }) {
1911
1625
  const width = statusBarWidth(terminalWidth);
@@ -1964,41 +1678,17 @@ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
1964
1678
  e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
1965
1679
  ].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
1966
1680
  }
1967
- async function handleModelCommand(command, runtime) {
1681
+ function handleModelCommand(command, runtime) {
1968
1682
  const current = runtime.engine.getModelSettings();
1969
1683
  const nextModel = command.model ?? current.model;
1970
1684
  const validationError = validateModelReasoningArgument(nextModel, command.reasoning);
1971
1685
  if (validationError)
1972
1686
  return { kind: "error", text: validationError };
1973
1687
  const reasoningUpdate = resolveModelReasoningUpdate(command.reasoning, current.reasoning, nextModel, command.model !== undefined);
1974
- const changed = command.model !== undefined || command.reasoning !== undefined;
1975
- if (changed) {
1688
+ if (command.model !== undefined || command.reasoning !== undefined) {
1976
1689
  runtime.engine.setModel(nextModel, reasoningUpdate.reasoning, reasoningUpdate.update);
1977
- try {
1978
- const { providerChanged } = await persistModelCommandSettings(runtime, command, reasoningUpdate);
1979
- if (providerChanged) {
1980
- const config = readModelProviderConfig(process.env);
1981
- if (config) {
1982
- const innerGateway = createModelGatewayFromConfig(config);
1983
- runtime.modelGateway.setInner(innerGateway);
1984
- runtime.agentRuntime.modelGateway = runtime.modelGateway;
1985
- runtime.engine.setModelProvider({
1986
- modelGateway: runtime.modelGateway,
1987
- model: config.model,
1988
- fallbackModel: config.fallbackModel,
1989
- reasoning: config.defaultReasoning,
1990
- });
1991
- syncImageGenerationTool(runtime, config.provider);
1992
- runtime.defaultReasoning = config.defaultReasoning;
1993
- }
1994
- }
1995
- }
1996
- catch (error) {
1997
- return { kind: "error", text: `Model settings changed for this session, but saving to ${runtime.envPath} failed: ${error instanceof Error ? error.message : String(error)}` };
1998
- }
1999
1690
  }
2000
- const settings = formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning);
2001
- return systemLine(changed ? `${settings}\nSaved to ${runtime.envPath}` : settings);
1691
+ return systemLine(formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning));
2002
1692
  }
2003
1693
  function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
2004
1694
  if (value === "off")
@@ -2012,62 +1702,6 @@ function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
2012
1702
  }
2013
1703
  return { reasoning: current, update: false };
2014
1704
  }
2015
- async function persistModelCommandSettings(runtime, command, reasoningUpdate) {
2016
- const currentProvider = currentModelProvider();
2017
- let targetProvider = currentProvider;
2018
- const updates = {};
2019
- if (command.model !== undefined) {
2020
- const metadata = findModelMetadata(command.model);
2021
- if (metadata) {
2022
- const modelProvider = parseLoginProvider(metadata.provider);
2023
- if (modelProvider) {
2024
- targetProvider = modelProvider;
2025
- if (targetProvider !== currentProvider)
2026
- updates.MODEL_PROVIDER = targetProvider;
2027
- }
2028
- }
2029
- updates[modelEnvKeyForProvider(targetProvider)] = command.model.trim() || undefined;
2030
- }
2031
- if (command.reasoning !== undefined || reasoningUpdate.update) {
2032
- updates.MODEL_REASONING_EFFORT = envValueForReasoning(reasoningUpdate.reasoning);
2033
- updates.MODEL_REASONING_SUMMARY = undefined;
2034
- }
2035
- if (Object.keys(updates).length === 0)
2036
- return { providerChanged: false };
2037
- await writeEnvUpdates(runtime.envPath, updates);
2038
- applyEnvUpdatesToProcess(updates);
2039
- runtime.defaultReasoning = reasoningUpdate.update ? reasoningUpdate.reasoning : runtime.defaultReasoning;
2040
- return { providerChanged: targetProvider !== currentProvider };
2041
- }
2042
- function currentModelProvider() {
2043
- return parseLoginProvider(process.env.MODEL_PROVIDER) ?? "openai";
2044
- }
2045
- function modelEnvKeyForProvider(provider) {
2046
- if (provider === "deepseek")
2047
- return "DEEPSEEK_MODEL";
2048
- if (provider === "kimi")
2049
- return "KIMI_MODEL";
2050
- return "OPENAI_MODEL";
2051
- }
2052
- function envValueForReasoning(reasoning) {
2053
- if (reasoning === null)
2054
- return "off";
2055
- return reasoning?.effort;
2056
- }
2057
- async function writeEnvUpdates(envPath, updates, removeKeys = []) {
2058
- await fs.mkdir(path.dirname(envPath), { recursive: true });
2059
- const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
2060
- const next = updateEnvContent(existing, updates, removeKeys);
2061
- await fs.writeFile(envPath, next, "utf8");
2062
- }
2063
- function applyEnvUpdatesToProcess(updates) {
2064
- for (const [key, value] of Object.entries(updates)) {
2065
- if (value === undefined)
2066
- delete process.env[key];
2067
- else
2068
- process.env[key] = value;
2069
- }
2070
- }
2071
1705
  function validateModelReasoningArgument(modelId, reasoning) {
2072
1706
  if (!reasoning || reasoning === "default" || reasoning === "off")
2073
1707
  return undefined;
@@ -2290,38 +1924,18 @@ function reduceStatus(status, event) {
2290
1924
  }
2291
1925
  return status;
2292
1926
  }
2293
- async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
1927
+ async function handleSessionsCommand(runtime, setBrowser, append) {
2294
1928
  const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
2295
1929
  if (sessions.length === 0) {
2296
1930
  setBrowser(undefined);
2297
1931
  append(systemLine("No saved sessions found."));
2298
1932
  return;
2299
1933
  }
2300
- setBrowser({ sessions, runningSessionIds, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2301
- }
2302
- async function handleExportCommand(command, runtime) {
2303
- const snapshot = runtime.engine.snapshot();
2304
- if (!snapshot.session)
2305
- throw new Error("session transcripts are disabled; cannot export current session");
2306
- const promptSnapshot = await runtime.engine.promptExportSnapshot();
2307
- const result = await writeSessionMarkdownExport({
2308
- outputPath: command.path,
2309
- session: snapshot.session,
2310
- agentId: snapshot.agentId,
2311
- promptSnapshot,
2312
- engineSnapshot: { ...snapshot, communicationLog: runtime.communicationLogger.snapshot(), usage: runtime.usage.snapshot() },
2313
- });
2314
- return systemLine(`Exported current session to ${result.outputPath}\nEntries: ${result.entries}\nMessages: ${result.messages}\nBytes: ${result.bytes}`);
1934
+ setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2315
1935
  }
2316
1936
  async function handleResumeCommand(sessionId, runtime, append) {
2317
1937
  try {
2318
- runtime.engine = runtime.engine.forkForSession(sessionId, true);
2319
- await runtime.engine.initialize();
2320
- const snapshot = runtime.engine.snapshot().session;
2321
- if (!snapshot)
2322
- throw new Error("session transcripts are disabled");
2323
- const metrics = await runtime.engine.contextMetrics();
2324
- return { snapshot, metrics };
1938
+ return await runtime.engine.resumeSession(sessionId);
2325
1939
  }
2326
1940
  catch (error) {
2327
1941
  append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
@@ -2346,7 +1960,6 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
2346
1960
  setBrowser({
2347
1961
  ...current,
2348
1962
  sessions: nextSessions,
2349
- runningSessionIds: current.runningSessionIds.filter((id) => id !== sessionId),
2350
1963
  pageIndex,
2351
1964
  selectedIndex: Math.min(current.selectedIndex, Math.max(0, pageLength - 1)),
2352
1965
  });
@@ -2363,11 +1976,11 @@ function initialLines(runtime, lineId) {
2363
1976
  ? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
2364
1977
  : "";
2365
1978
  const lines = [
2366
- { id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}\n${formatTipLine(tipAt(initialTipIndex(session?.sessionId ?? process.cwd())))}`, previewStyle: "summary" },
1979
+ { id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}`, previewStyle: "summary" },
2367
1980
  ];
2368
1981
  lineId.current = 0;
2369
1982
  if (runtime.envNotice)
2370
- lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
1983
+ lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, previewStyle: "summary" });
2371
1984
  for (const line of restoredHistoryLines(runtime))
2372
1985
  lines.push({ id: ++lineId.current, ...line });
2373
1986
  return lines;
@@ -2386,71 +1999,6 @@ function restoredHistoryLines(runtime) {
2386
1999
  }
2387
2000
  return lines;
2388
2001
  }
2389
- const LOGIN_PROVIDERS = ["openai", "deepseek", "kimi"];
2390
- const SHARED_LOGIN_FIELDS = [
2391
- { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
2392
- { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
2393
- { key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
2394
- { key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
2395
- { key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
2396
- { key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
2397
- ];
2398
- const LOGIN_FIELD_DEFINITIONS = {
2399
- openai: [
2400
- { key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2401
- { key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
2402
- { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
2403
- { key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
2404
- { key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
2405
- ...SHARED_LOGIN_FIELDS,
2406
- ],
2407
- deepseek: [
2408
- { key: "apiKey", label: "API key", envKey: "DEEPSEEK_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2409
- { key: "baseUrl", label: "Base URL", envKey: "DEEPSEEK_BASE_URL", scope: "provider", placeholder: "https://api.deepseek.com" },
2410
- { key: "model", label: "Model", envKey: "DEEPSEEK_MODEL", scope: "provider", required: true, placeholder: "deepseek-chat" },
2411
- { key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
2412
- ...SHARED_LOGIN_FIELDS,
2413
- ],
2414
- kimi: [
2415
- { key: "apiKey", label: "API key", envKey: "KIMI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2416
- { key: "baseUrl", label: "Base URL", envKey: "KIMI_BASE_URL", scope: "provider", placeholder: "https://api.moonshot.cn/v1" },
2417
- { key: "model", label: "Model", envKey: "KIMI_MODEL", scope: "provider", required: true, placeholder: "kimi-k2.6" },
2418
- { key: "fallbackModel", label: "Fallback model", envKey: "KIMI_FALLBACK_MODEL", scope: "provider" },
2419
- ...SHARED_LOGIN_FIELDS,
2420
- ],
2421
- };
2422
- const DEPRECATED_MODEL_ENV_KEYS = [
2423
- "MODEL_API_KEY",
2424
- "MODEL_BASE_URL",
2425
- "MODEL_ID",
2426
- "MODEL_FALLBACK_ID",
2427
- "MODEL_ENDPOINT",
2428
- "OPENAI_PROVIDER",
2429
- "OPENAI_REASONING_EFFORT",
2430
- "OPENAI_REASONING_SUMMARY",
2431
- "OPENAI_MAX_OUTPUT_TOKENS",
2432
- "OPENAI_TIMEOUT_MS",
2433
- "OPENAI_STREAM_IDLE_TIMEOUT_MS",
2434
- "OPENAI_MAX_RETRIES",
2435
- "DEEPSEEK_REASONING_EFFORT",
2436
- "DEEPSEEK_REASONING_SUMMARY",
2437
- "DEEPSEEK_MAX_OUTPUT_TOKENS",
2438
- "DEEPSEEK_TIMEOUT_MS",
2439
- "DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
2440
- "DEEPSEEK_MAX_RETRIES",
2441
- "KIMI_REASONING_EFFORT",
2442
- "KIMI_REASONING_SUMMARY",
2443
- "KIMI_MAX_OUTPUT_TOKENS",
2444
- "KIMI_TIMEOUT_MS",
2445
- "KIMI_STREAM_IDLE_TIMEOUT_MS",
2446
- "KIMI_MAX_RETRIES",
2447
- "MOONSHOT_REASONING_EFFORT",
2448
- "MOONSHOT_REASONING_SUMMARY",
2449
- "MOONSHOT_MAX_OUTPUT_TOKENS",
2450
- "MOONSHOT_TIMEOUT_MS",
2451
- "MOONSHOT_STREAM_IDLE_TIMEOUT_MS",
2452
- "MOONSHOT_MAX_RETRIES",
2453
- ];
2454
2002
  function sessionsPageCount(state) {
2455
2003
  return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
2456
2004
  }
@@ -2493,363 +2041,23 @@ function SessionsBrowser({ state, width }) {
2493
2041
  return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
2494
2042
  const selected = index === state.selectedIndex;
2495
2043
  const absoluteIndex = state.pageIndex * state.pageSize + index;
2496
- const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth, state.runningSessionIds.includes(session.sessionId));
2044
+ const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth);
2497
2045
  return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
2498
2046
  color: selected ? "black" : "white",
2499
2047
  backgroundColor: selected ? "cyan" : undefined,
2500
2048
  }, row.numberPrefix), row.rest);
2501
2049
  }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
2502
2050
  }
2503
- function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
2504
- if (key.escape) {
2505
- if (state.step === "fields")
2506
- setLoginFormState({ ...state, step: "provider" });
2507
- else {
2508
- setLoginFormState(undefined);
2509
- append(systemLine("Login cancelled."));
2510
- }
2511
- return;
2512
- }
2513
- if (state.step === "provider") {
2514
- if (key.upArrow) {
2515
- setLoginFormState(moveLoginProviderSelection(state, -1));
2516
- return;
2517
- }
2518
- if (key.downArrow) {
2519
- setLoginFormState(moveLoginProviderSelection(state, 1));
2520
- return;
2521
- }
2522
- if (key.return) {
2523
- const provider = state.providers[state.selectedProviderIndex] ?? state.provider;
2524
- setLoginFormState({ ...loginFormForProvider(provider, state.envPath), step: "fields" });
2525
- return;
2526
- }
2527
- return;
2528
- }
2529
- const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
2530
- const field = fields[state.selectedFieldIndex];
2531
- if (!field)
2532
- return;
2533
- if (key.upArrow) {
2534
- setLoginFormState(moveLoginFieldSelection(state, -1));
2535
- return;
2536
- }
2537
- if (key.downArrow) {
2538
- setLoginFormState(moveLoginFieldSelection(state, 1));
2539
- return;
2540
- }
2541
- if (key.leftArrow) {
2542
- setLoginFormState({ ...state, cursor: Math.max(0, state.cursor - 1) });
2543
- return;
2544
- }
2545
- if (key.rightArrow) {
2546
- const current = state.values[field.key] ?? "";
2547
- setLoginFormState({ ...state, cursor: Math.min(current.length, state.cursor + 1) });
2548
- return;
2549
- }
2550
- if (key.tab && field.options?.length) {
2551
- setLoginFormState(cycleLoginFieldOption(state, field));
2552
- return;
2553
- }
2554
- if (key.backspace || key.delete) {
2555
- setLoginFormState(deleteLoginFieldCharacter(state, field));
2556
- return;
2557
- }
2558
- if (key.return) {
2559
- void submitLoginForm(state, runtime, append, setLoginFormState, setStatus);
2560
- return;
2561
- }
2562
- if (value && !key.ctrl && !key.meta) {
2563
- setLoginFormState(insertLoginFieldText(state, field, value));
2564
- }
2565
- }
2566
- function moveLoginProviderSelection(state, delta) {
2567
- const selectedProviderIndex = (state.selectedProviderIndex + delta + state.providers.length) % state.providers.length;
2568
- return { ...state, selectedProviderIndex, provider: state.providers[selectedProviderIndex] ?? state.provider };
2569
- }
2570
- function moveLoginFieldSelection(state, delta) {
2571
- const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
2572
- const selectedFieldIndex = (state.selectedFieldIndex + delta + fields.length) % fields.length;
2573
- const field = fields[selectedFieldIndex];
2574
- return { ...state, selectedFieldIndex, cursor: field ? (state.values[field.key] ?? "").length : 0 };
2575
- }
2576
- function cycleLoginFieldOption(state, field) {
2577
- const options = field.options ?? [];
2578
- const current = state.values[field.key] ?? "";
2579
- const index = options.indexOf(current);
2580
- const next = options[(index + 1 + options.length) % options.length] ?? "";
2581
- return { ...state, values: { ...state.values, [field.key]: next }, cursor: next.length };
2582
- }
2583
- function insertLoginFieldText(state, field, value) {
2584
- const current = state.values[field.key] ?? "";
2585
- const cursor = Math.max(0, Math.min(state.cursor, current.length));
2586
- const next = `${current.slice(0, cursor)}${value}${current.slice(cursor)}`;
2587
- return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor + value.length };
2588
- }
2589
- function deleteLoginFieldCharacter(state, field) {
2590
- const current = state.values[field.key] ?? "";
2591
- const cursor = Math.max(0, Math.min(state.cursor, current.length));
2592
- if (cursor <= 0)
2593
- return state;
2594
- const next = `${current.slice(0, cursor - 1)}${current.slice(cursor)}`;
2595
- return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor - 1 };
2596
- }
2597
- async function submitLoginForm(state, runtime, append, setLoginFormState, setStatus) {
2598
- const validationError = validateLoginForm(state);
2599
- if (validationError) {
2600
- append({ kind: "error", text: validationError });
2601
- return;
2602
- }
2603
- try {
2604
- await saveLoginFormToEnv(state);
2605
- applyLoginFormToProcessEnv(state);
2606
- const config = readModelProviderConfig(process.env);
2607
- if (!config)
2608
- throw new Error("Saved provider config could not be loaded from environment.");
2609
- const innerGateway = createModelGatewayFromConfig(config);
2610
- runtime.modelGateway.setInner(innerGateway);
2611
- runtime.agentRuntime.modelGateway = runtime.modelGateway;
2612
- runtime.engine.setModelProvider({
2613
- modelGateway: runtime.modelGateway,
2614
- model: config.model,
2615
- fallbackModel: config.fallbackModel,
2616
- reasoning: config.defaultReasoning,
2617
- });
2618
- syncImageGenerationTool(runtime, config.provider);
2619
- runtime.defaultReasoning = config.defaultReasoning;
2620
- const metrics = await runtime.engine.contextMetrics();
2621
- setStatus((current) => ({
2622
- ...current,
2623
- metrics,
2624
- activityTick: current.activityTick + 1,
2625
- }));
2626
- setLoginFormState(undefined);
2627
- append(systemLine(`Saved ${state.provider} login to ${state.envPath}\n${formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning)}`, EXPANDED_SUMMARY_MAX_LINES));
2628
- }
2629
- catch (error) {
2630
- append({ kind: "error", text: `Login save failed: ${error instanceof Error ? error.message : String(error)}` });
2631
- }
2632
- }
2633
- function validateLoginForm(state) {
2634
- for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
2635
- const value = (state.values[field.key] ?? "").trim();
2636
- if (field.required && !value)
2637
- return `${field.label} is required.`;
2638
- if (field.options?.length && value && !field.options.includes(value))
2639
- return `${field.label} must be one of: ${field.options.filter(Boolean).join(", ")}`;
2640
- }
2641
- for (const fieldKey of ["maxOutputTokens", "timeoutMs", "streamIdleTimeoutMs", "maxRetries"]) {
2642
- const value = state.values[fieldKey]?.trim();
2643
- if (value && !Number.isFinite(Number(value)))
2644
- return `${fieldKey} must be a number.`;
2645
- }
2646
- return undefined;
2647
- }
2648
- function createLoginFormState(envPath = getUserDotEnvPath()) {
2649
- const env = parseEnvFileSafe(envPath);
2650
- const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
2651
- return loginFormForProvider(currentProvider, envPath, env);
2652
- }
2653
- function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
2654
- const selectedProviderIndex = Math.max(0, LOGIN_PROVIDERS.indexOf(provider));
2655
- return {
2656
- step: "provider",
2657
- providers: LOGIN_PROVIDERS,
2658
- selectedProviderIndex,
2659
- provider,
2660
- selectedFieldIndex: 0,
2661
- cursor: 0,
2662
- values: loginValuesForProvider(provider, env),
2663
- envPath,
2664
- };
2665
- }
2666
- function loginValuesForProvider(provider, env) {
2667
- const values = {};
2668
- for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
2669
- values[field.key] = env[field.envKey] ?? "";
2670
- }
2671
- if (provider === "kimi") {
2672
- values.apiKey ||= env.MOONSHOT_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "";
2673
- values.baseUrl ||= env.MOONSHOT_BASE_URL ?? process.env.MOONSHOT_BASE_URL ?? "";
2674
- values.model ||= env.MOONSHOT_MODEL ?? process.env.MOONSHOT_MODEL ?? "";
2675
- values.fallbackModel ||= env.MOONSHOT_FALLBACK_MODEL ?? process.env.MOONSHOT_FALLBACK_MODEL ?? "";
2676
- }
2677
- if (!values.baseUrl)
2678
- values.baseUrl = defaultBaseUrlForLoginProvider(provider);
2679
- if (!values.model)
2680
- values.model = defaultModelForLoginProvider(provider);
2681
- if (provider === "openai" && !values.endpoint)
2682
- values.endpoint = "auto";
2683
- return values;
2684
- }
2685
- function parseLoginProvider(value) {
2686
- if (value === "openai" || value === "deepseek" || value === "kimi")
2687
- return value;
2688
- return undefined;
2689
- }
2690
- function guessLoginProvider(env) {
2691
- if (env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY)
2692
- return "kimi";
2693
- if (env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY)
2694
- return "deepseek";
2695
- return "openai";
2696
- }
2697
- function defaultBaseUrlForLoginProvider(provider) {
2698
- if (provider === "deepseek")
2699
- return "https://api.deepseek.com";
2700
- if (provider === "kimi")
2701
- return "https://api.moonshot.cn/v1";
2702
- return "https://api.openai.com";
2703
- }
2704
- function defaultModelForLoginProvider(provider) {
2705
- if (provider === "deepseek")
2706
- return "deepseek-chat";
2707
- if (provider === "kimi")
2708
- return "kimi-k2.6";
2709
- return "gpt-5.5";
2710
- }
2711
- function loginFormViewHeight(state) {
2712
- return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
2713
- }
2714
- function LoginFormView({ state, width }) {
2715
- const contentWidth = Math.max(30, width);
2716
- if (state.step === "provider") {
2717
- return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: choose provider · saving to ${state.envPath}`, contentWidth)), ...state.providers.map((provider, index) => e(Text, { key: provider, color: "white" }, e(Text, { color: index === state.selectedProviderIndex ? "black" : "white", backgroundColor: index === state.selectedProviderIndex ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: "gray" }, " "), e(Text, { color: "cyan" }, provider))), e(Text, { color: "gray" }, fitToWidth("↑/↓ select · Enter edit config · Esc close", contentWidth)));
2718
- }
2719
- const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
2720
- const maxLabel = Math.max(...fields.map((field) => field.label.length));
2721
- return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: ${state.provider} · ${state.envPath}`, contentWidth)), ...fields.map((field, index) => {
2722
- const selected = index === state.selectedFieldIndex;
2723
- const rawValue = state.values[field.key] ?? "";
2724
- const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
2725
- const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
2726
- return e(Text, { key: field.key, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: field.required ? "yellow" : "gray" }, ` ${field.label.padEnd(maxLabel)} `), e(Text, { color: field.scope === "shared" ? "blue" : "gray" }, field.scope === "shared" ? "shared " : "provider "), e(Text, { color: rawValue ? "white" : "gray" }, fitToWidth(`${visibleValue}${placeholder}`, Math.max(8, contentWidth - maxLabel - 14))));
2727
- }), e(Text, { color: "gray" }, fitToWidth("↑/↓ field · ←/→ cursor · type edit · Tab cycle choices · Enter save · Esc back/cancel", contentWidth)), e(Text, { color: "gray" }, fitToWidth("Provider fields save as OPENAI_* / DEEPSEEK_* / KIMI_*; shared runtime fields save as MODEL_*.", contentWidth)));
2728
- }
2729
- function formatLoginFieldValue(field, value, cursor) {
2730
- const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
2731
- if (cursor === undefined)
2732
- return display;
2733
- const safeCursor = Math.max(0, Math.min(cursor, display.length));
2734
- const selected = display[safeCursor] ?? " ";
2735
- return `${display.slice(0, safeCursor)}█${selected === " " ? "" : display.slice(safeCursor + 1)}`;
2736
- }
2737
- function applyLoginFormToProcessEnv(state) {
2738
- applyEnvUpdatesToProcess(envEntriesForLoginForm(state));
2739
- for (const key of DEPRECATED_MODEL_ENV_KEYS)
2740
- delete process.env[key];
2741
- }
2742
- async function saveLoginFormToEnv(state) {
2743
- await writeEnvUpdates(state.envPath, envEntriesForLoginForm(state), DEPRECATED_MODEL_ENV_KEYS);
2744
- }
2745
- function envEntriesForLoginForm(state) {
2746
- const entries = {
2747
- MODEL_PROVIDER: state.provider,
2748
- };
2749
- for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
2750
- const value = (state.values[field.key] ?? "").trim();
2751
- entries[field.envKey] = value || undefined;
2752
- }
2753
- if (state.provider === "kimi") {
2754
- entries.MOONSHOT_API_KEY = undefined;
2755
- entries.MOONSHOT_BASE_URL = undefined;
2756
- entries.MOONSHOT_MODEL = undefined;
2757
- entries.MOONSHOT_FALLBACK_MODEL = undefined;
2758
- }
2759
- return entries;
2760
- }
2761
- function updateEnvContent(content, updates, removeKeys = []) {
2762
- const keys = new Set(Object.keys(updates));
2763
- const removals = new Set(removeKeys);
2764
- const seen = new Set();
2765
- const lines = content ? content.split(/\r?\n/) : [];
2766
- const updatedLines = lines.map((line) => {
2767
- const parsed = parseEnvLine(line);
2768
- if (!parsed)
2769
- return line;
2770
- if (removals.has(parsed.key) && !keys.has(parsed.key))
2771
- return undefined;
2772
- if (!keys.has(parsed.key))
2773
- return line;
2774
- seen.add(parsed.key);
2775
- const value = updates[parsed.key];
2776
- if (value === undefined)
2777
- return undefined;
2778
- return `${parsed.key}=${quoteEnvValue(value)}`;
2779
- }).filter((line) => line !== undefined);
2780
- const missing = Object.entries(updates).filter((entry) => !seen.has(entry[0]) && entry[1] !== undefined);
2781
- if (missing.length > 0) {
2782
- const grouped = groupLoginEnvEntries(missing);
2783
- appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
2784
- appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
2785
- appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
2786
- appendEnvGroup(updatedLines, "# Kimi provider settings", grouped.kimi);
2787
- appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
2788
- }
2789
- return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
2790
- }
2791
- function groupLoginEnvEntries(entries) {
2792
- return {
2793
- active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
2794
- openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
2795
- deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
2796
- kimi: entries.filter(([key]) => key.startsWith("KIMI_") || key.startsWith("MOONSHOT_")),
2797
- shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
2798
- };
2799
- }
2800
- function appendEnvGroup(lines, header, entries) {
2801
- if (entries.length === 0)
2802
- return;
2803
- if (lines.length > 0 && lines[lines.length - 1]?.trim())
2804
- lines.push("");
2805
- lines.push(header);
2806
- for (const [key, value] of entries)
2807
- lines.push(`${key}=${quoteEnvValue(value)}`);
2808
- }
2809
- function parseEnvFileSafe(envPath) {
2810
- if (!existsSync(envPath))
2811
- return {};
2812
- const env = {};
2813
- for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
2814
- const parsed = parseEnvLine(line);
2815
- if (parsed)
2816
- env[parsed.key] = stripEnvQuotes(parsed.value.trim());
2817
- }
2818
- return env;
2819
- }
2820
- function parseEnvLine(line) {
2821
- const trimmed = line.trim();
2822
- if (!trimmed || trimmed.startsWith("#"))
2823
- return undefined;
2824
- const separator = trimmed.indexOf("=");
2825
- if (separator <= 0)
2826
- return undefined;
2827
- const key = trimmed.slice(0, separator).trim();
2828
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
2829
- return undefined;
2830
- return { key, value: trimmed.slice(separator + 1) };
2831
- }
2832
- function quoteEnvValue(value) {
2833
- if (/^[A-Za-z0-9_./:@+-]*$/.test(value))
2834
- return value;
2835
- return JSON.stringify(value);
2836
- }
2837
- function stripEnvQuotes(value) {
2838
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
2839
- return value.slice(1, -1);
2840
- return value;
2841
- }
2842
- function formatSessionBrowserRow(session, absoluteIndex, width, running = false) {
2051
+ function formatSessionBrowserRow(session, absoluteIndex, width) {
2843
2052
  const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
2844
2053
  const title = session.title?.trim() || "(untitled)";
2845
- const runningTag = running ? " · running" : "";
2846
2054
  const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
2847
2055
  const messages = ` · ${session.messages} messages`;
2848
- const fixedParts = `${numberPrefix} ${runningTag}${updated}${messages}`;
2056
+ const fixedParts = `${numberPrefix} ${updated}${messages}`;
2849
2057
  const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
2850
2058
  const id = truncateMiddle(session.sessionId, idBudget);
2851
2059
  const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
2852
- const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${runningTag}${updated}${messages}`, width);
2060
+ const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${updated}${messages}`, width);
2853
2061
  return { numberPrefix, rest: row.slice(numberPrefix.length) };
2854
2062
  }
2855
2063
  function formatSessionTimestamp(value) {
@@ -2928,7 +2136,7 @@ function kindForRole(role) {
2928
2136
  }
2929
2137
  function titleForKind(kind) {
2930
2138
  if (kind === "thinking")
2931
- return `${THINKING_MARKER} think`;
2139
+ return `${THINKING_MARKER} Think`;
2932
2140
  if (kind === "tool")
2933
2141
  return "Tool";
2934
2142
  if (kind === "error")
@@ -2982,7 +2190,6 @@ function formatToolUse(toolUse) {
2982
2190
  return {
2983
2191
  kind: "tool",
2984
2192
  title: toolTitle(toolUse.name, "running"),
2985
- bodyTitle: planToolBodyTitle(toolUse.input),
2986
2193
  text: formatPlanToolPayload(toolUse.input),
2987
2194
  };
2988
2195
  }
@@ -2998,7 +2205,6 @@ function formatToolResultLine(toolName, output, ok) {
2998
2205
  const line = {
2999
2206
  kind: ok ? "tool" : "error",
3000
2207
  title: toolTitle(toolName, "finished"),
3001
- bodyTitle: formatted.bodyTitle,
3002
2208
  titleStatus: ok ? "success" : "failure",
3003
2209
  text: formatted.text,
3004
2210
  format: formatted.format,
@@ -3040,12 +2246,10 @@ function isPlanToolPayload(value) {
3040
2246
  (item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
3041
2247
  });
3042
2248
  }
3043
- function planToolBodyTitle(payload) {
3044
- const title = payload.title?.trim();
3045
- return title ? title : undefined;
3046
- }
3047
2249
  function formatPlanToolPayload(payload) {
3048
2250
  const sections = [];
2251
+ if (payload.title?.trim())
2252
+ sections.push(`**${payload.title.trim()}**`);
3049
2253
  if (payload.summary?.trim())
3050
2254
  sections.push(payload.summary.trim());
3051
2255
  if (payload.note?.trim())
@@ -3133,11 +2337,26 @@ function isReplScalar(value) {
3133
2337
  return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
3134
2338
  }
3135
2339
  function formatToolResult(toolName, output, ok) {
3136
- if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output)) {
2340
+ if (toolName === "edit" && isRecord(output) && isEditToolOutput(output)) {
3137
2341
  return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
3138
2342
  }
3139
2343
  if (isExecOutput(output)) {
3140
- return { text: formatExecToolResult(output, ok), format: "ansi", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2344
+ const status = output.timedOut
2345
+ ? "timed out"
2346
+ : output.exitCode === 0
2347
+ ? "exit 0"
2348
+ : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
2349
+ const sections = [
2350
+ `${status} · ${output.durationMs}ms`,
2351
+ `$ ${output.command}`,
2352
+ ];
2353
+ if (output.stdout)
2354
+ sections.push("stdout:", output.stdout.replace(/\s+$/u, ""));
2355
+ if (output.stderr)
2356
+ sections.push("stderr:", output.stderr.replace(/\s+$/u, ""));
2357
+ if (!output.stdout && !output.stderr)
2358
+ sections.push(ok ? "no output" : "no captured output");
2359
+ return { text: sections.join("\n"), format: "ansi" };
3141
2360
  }
3142
2361
  if (typeof output === "string" && hasAnsi(output)) {
3143
2362
  return { text: output, format: "ansi" };
@@ -3154,11 +2373,8 @@ function formatToolResult(toolName, output, ok) {
3154
2373
  if (toolName === "search" && isRecord(output)) {
3155
2374
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
3156
2375
  }
3157
- if (toolName === "image2" && isRecord(output)) {
3158
- return { text: formatImageGenerationToolResult(output, ok), summaryMaxLines: 4 };
3159
- }
3160
2376
  if (toolName === "plan" && isPlanToolPayload(output)) {
3161
- return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
2377
+ return { text: formatPlanToolPayload(output), full: true };
3162
2378
  }
3163
2379
  return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
3164
2380
  }
@@ -3247,54 +2463,9 @@ function isExecOutput(value) {
3247
2463
  typeof record.stdout === "string" &&
3248
2464
  typeof record.stderr === "string");
3249
2465
  }
3250
- function formatExecToolResult(output, ok) {
3251
- const status = output.timedOut
3252
- ? "timed out"
3253
- : output.exitCode === 0
3254
- ? "exit 0"
3255
- : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
3256
- const lines = [
3257
- "exec result",
3258
- `status: ${status}`,
3259
- `duration: ${output.durationMs}ms`,
3260
- `command: ${output.command}`,
3261
- ];
3262
- const stdout = output.stdout.replace(/\s+$/u, "");
3263
- const stderr = output.stderr.replace(/\s+$/u, "");
3264
- if (stdout)
3265
- lines.push("stdout:", stdout);
3266
- if (stderr)
3267
- lines.push("stderr:", stderr);
3268
- if (!stdout && !stderr)
3269
- lines.push(ok ? "output: (none)" : "output: (not captured)");
3270
- return lines.join("\n");
3271
- }
3272
2466
  function isRecord(value) {
3273
2467
  return !!value && typeof value === "object" && !Array.isArray(value);
3274
2468
  }
3275
- function formatImageGenerationToolResult(output, ok) {
3276
- const error = typeof output.error === "string" ? output.error : undefined;
3277
- if (!ok || error)
3278
- return ["image generation failed", error ?? formatReplData(output, 1200)].join("\n");
3279
- const provider = typeof output.provider === "string" ? output.provider : "openai";
3280
- const model = typeof output.model === "string" ? output.model : undefined;
3281
- const returnedImages = typeof output.returnedImages === "number" ? output.returnedImages : Array.isArray(output.images) ? output.images.length : undefined;
3282
- const size = typeof output.size === "string" ? output.size : undefined;
3283
- const quality = typeof output.quality === "string" ? output.quality : undefined;
3284
- const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
3285
- const lines = [`generated ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
3286
- const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
3287
- if (details.length > 0)
3288
- lines.push(details.join(" · "));
3289
- const duration = imageGenerationDuration(output);
3290
- if (duration !== undefined)
3291
- lines.push(`duration: ${duration}ms`);
3292
- return lines.join("\n");
3293
- }
3294
- function imageGenerationDuration(output) {
3295
- const value = output.duration ?? output.elapsed ?? output.durationMs ?? output.elapsedMs;
3296
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.round(value)) : undefined;
3297
- }
3298
2469
  function formatListToolResult(output, ok) {
3299
2470
  const pathValue = typeof output.path === "string" ? output.path : "";
3300
2471
  const typeValue = typeof output.type === "string" ? output.type : "result";
@@ -3442,9 +2613,11 @@ function formatGrepContextLine(line, marker) {
3442
2613
  }
3443
2614
  function renderContextParts(metrics) {
3444
2615
  if (!metrics)
3445
- return { percent: "?" };
2616
+ return { used: "?", limit: "?", percent: "?" };
2617
+ const used = compactNumber(metrics.estimatedInputTokens);
2618
+ const limit = metrics.contextWindowTokens ? compactNumber(metrics.contextWindowTokens) : "?";
3446
2619
  const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
3447
- return { percent };
2620
+ return { used, limit, percent };
3448
2621
  }
3449
2622
  function contextColor(metrics) {
3450
2623
  const ratio = metrics?.contextUsageRatio;
@@ -3727,8 +2900,9 @@ function isFullWidthCodePoint(codePoint) {
3727
2900
  (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
3728
2901
  }
3729
2902
  const SESSIONS_DEFAULT_PAGE_SIZE = 10;
3730
- const TERMINAL_TITLE_WORKING_PREFIX = "● ";
3731
- const TERMINAL_TITLE_READY_PREFIX = "";
2903
+ const TERMINAL_TITLE_DOT_FILLED_PREFIX = "● ";
2904
+ const TERMINAL_TITLE_DOT_BLANK_PREFIX = " ";
2905
+ const TERMINAL_TITLE_BLINK_INTERVAL_MS = 1000;
3732
2906
  const REPL_ANIMATION_INTERVAL_MS = 420;
3733
2907
  const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
3734
2908
  const TOKEN_PULSE_MS = 900;