neoctl 0.1.6 → 0.1.7

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 (47) hide show
  1. package/README.md +18 -9
  2. package/dist/agents/local-agent-task.js +2 -1
  3. package/dist/agents/local-agent-task.js.map +1 -1
  4. package/dist/core/query.js +34 -1
  5. package/dist/core/query.js.map +1 -1
  6. package/dist/core/smoke-core-loop.js +34 -3
  7. package/dist/core/smoke-core-loop.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/model/config.d.ts +5 -2
  12. package/dist/model/config.js +21 -1
  13. package/dist/model/config.js.map +1 -1
  14. package/dist/model/env.js +10 -6
  15. package/dist/model/env.js.map +1 -1
  16. package/dist/model/kimi-adapter.d.ts +29 -0
  17. package/dist/model/kimi-adapter.js +108 -0
  18. package/dist/model/kimi-adapter.js.map +1 -0
  19. package/dist/model/model-metadata.json +51 -2
  20. package/dist/model/openai-chat-mapper.d.ts +1 -0
  21. package/dist/model/openai-chat-mapper.js +7 -3
  22. package/dist/model/openai-chat-mapper.js.map +1 -1
  23. package/dist/model/provider-factory.js +16 -0
  24. package/dist/model/provider-factory.js.map +1 -1
  25. package/dist/open-directory.d.ts +1 -0
  26. package/dist/open-directory.js +26 -0
  27. package/dist/open-directory.js.map +1 -0
  28. package/dist/paths.d.ts +7 -0
  29. package/dist/paths.js +12 -0
  30. package/dist/paths.js.map +1 -0
  31. package/dist/repl/commands.d.ts +2 -0
  32. package/dist/repl/commands.js +3 -0
  33. package/dist/repl/commands.js.map +1 -1
  34. package/dist/repl/index.js +168 -30
  35. package/dist/repl/index.js.map +1 -1
  36. package/dist/session/session-store.js +2 -2
  37. package/dist/session/session-store.js.map +1 -1
  38. package/dist/tips.d.ts +10 -0
  39. package/dist/tips.js +168 -0
  40. package/dist/tips.js.map +1 -0
  41. package/dist/web/html.d.ts +1 -0
  42. package/dist/web/html.js +697 -0
  43. package/dist/web/html.js.map +1 -0
  44. package/dist/web/index.d.ts +2 -0
  45. package/dist/web/index.js +1465 -0
  46. package/dist/web/index.js.map +1 -0
  47. package/package.json +4 -1
@@ -27,6 +27,8 @@ import { isModelReasoningArgument, isValidReplCommandLine, parseReplCommand, hel
27
27
  import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
28
28
  import { writeSessionMarkdownExport } from "../session/session-export.js";
29
29
  import { readClipboard } from "./clipboard.js";
30
+ import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
31
+ import { openDirectory } from "../open-directory.js";
30
32
  const e = React.createElement;
31
33
  class SessionUsageTracker {
32
34
  totals = emptyUsageTotals();
@@ -172,7 +174,7 @@ async function createRuntime() {
172
174
  };
173
175
  }
174
176
  function formatCreatedEnvNotice(path) {
175
- return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY), then restart neo.`;
177
+ 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.`;
176
178
  }
177
179
  function parseResumeFlag(value) {
178
180
  if (!value)
@@ -361,6 +363,7 @@ function InkRepl({ runtime }) {
361
363
  const queuedAttachmentsRef = useRef(undefined);
362
364
  const [cursor, setCursor] = useState(0);
363
365
  const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
366
+ const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
364
367
  const [busy, setBusy] = useState(false);
365
368
  const [status, setStatus] = useState(() => initialStatus(runtime));
366
369
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
@@ -480,9 +483,11 @@ function InkRepl({ runtime }) {
480
483
  }, PASTE_STATUS_DISPLAY_MS);
481
484
  pasteStatusTimerRef.current = timer;
482
485
  };
486
+ const advanceTip = () => setTipIndex((current) => current + 1);
483
487
  const insertAtCursor = (value) => {
484
488
  const currentText = inputRef.current;
485
489
  const currentCursor = cursorRef.current;
490
+ advanceTip();
486
491
  setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
487
492
  };
488
493
  const insertAttachmentLabel = (attachment) => {
@@ -842,6 +847,18 @@ function InkRepl({ runtime }) {
842
847
  }
843
848
  return;
844
849
  }
850
+ if (command.type === "env") {
851
+ const envDirectory = path.dirname(runtime.envPath);
852
+ try {
853
+ await fs.mkdir(envDirectory, { recursive: true });
854
+ await openDirectory(envDirectory);
855
+ append({ kind: "system", title: "System", text: `Opened env directory: ${envDirectory}`, format: "plain", previewStyle: "summary" });
856
+ }
857
+ catch (error) {
858
+ append({ kind: "error", text: `Failed to open env directory ${envDirectory}: ${error instanceof Error ? error.message : String(error)}`, format: "plain" });
859
+ }
860
+ return;
861
+ }
845
862
  if (command.type === "sessions") {
846
863
  await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
847
864
  return;
@@ -939,6 +956,7 @@ function InkRepl({ runtime }) {
939
956
  }
940
957
  };
941
958
  useEffect(() => {
959
+ setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
942
960
  setLines(initialLines(runtime, lineId));
943
961
  assistantLineId.current = undefined;
944
962
  thinkingLineId.current = undefined;
@@ -955,9 +973,13 @@ function InkRepl({ runtime }) {
955
973
  const width = terminalSize.columns;
956
974
  const inputLockedByQueue = busy && queuedInput !== undefined;
957
975
  const prompt = promptPrefix(busy);
958
- const promptDisplayText = input.length === 0 && promptPlaceholder ? promptPlaceholder : input;
959
- const promptDisplayCursor = input.length === 0 && promptPlaceholder ? promptPlaceholder.length : cursor;
960
- const slashCompletions = inputLockedByQueue || promptPlaceholder || loginForm ? [] : slashCommandCompletions(input, cursor);
976
+ const currentTip = tipAt(tipIndex);
977
+ const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
978
+ const promptDisplayText = input;
979
+ const promptDisplayCursor = cursor;
980
+ const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
981
+ const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
982
+ const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
961
983
  const visibleSlashCompletionCount = slashCompletions.length;
962
984
  const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
963
985
  ? 0
@@ -965,7 +987,7 @@ function InkRepl({ runtime }) {
965
987
  if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
966
988
  slashCompletionIndexRef.current = selectedSlashCompletionIndex;
967
989
  }
968
- const promptHeight = promptTextView(promptDisplayText, promptDisplayCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
990
+ const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
969
991
  const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
970
992
  const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
971
993
  const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
@@ -1088,6 +1110,10 @@ function InkRepl({ runtime }) {
1088
1110
  if (key.backspace || key.delete) {
1089
1111
  const currentText = inputRef.current;
1090
1112
  const currentCursor = cursorRef.current;
1113
+ if (currentText.length === 0) {
1114
+ setTipIndex((current) => current + 1);
1115
+ return;
1116
+ }
1091
1117
  if (currentCursor > 0) {
1092
1118
  setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
1093
1119
  }
@@ -1099,6 +1125,10 @@ function InkRepl({ runtime }) {
1099
1125
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1100
1126
  return;
1101
1127
  }
1128
+ if (inputRef.current.length === 0) {
1129
+ setTipIndex((current) => current - 1);
1130
+ return;
1131
+ }
1102
1132
  setPromptState(inputRef.current, cursorRef.current - 1);
1103
1133
  return;
1104
1134
  }
@@ -1108,18 +1138,32 @@ function InkRepl({ runtime }) {
1108
1138
  setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1109
1139
  return;
1110
1140
  }
1141
+ if (inputRef.current.length === 0) {
1142
+ setTipIndex((current) => current + 1);
1143
+ return;
1144
+ }
1111
1145
  setPromptState(inputRef.current, cursorRef.current + 1);
1112
1146
  return;
1113
1147
  }
1114
1148
  if (key.home) {
1115
- setPromptState(inputRef.current, 0);
1149
+ if (inputRef.current.length === 0)
1150
+ setTipIndex(0);
1151
+ else
1152
+ setPromptState(inputRef.current, 0);
1116
1153
  return;
1117
1154
  }
1118
1155
  if (key.end) {
1119
- setPromptState(inputRef.current, inputRef.current.length);
1156
+ if (inputRef.current.length === 0)
1157
+ setTipIndex((current) => current + 1);
1158
+ else
1159
+ setPromptState(inputRef.current, inputRef.current.length);
1120
1160
  return;
1121
1161
  }
1122
1162
  if (key.upArrow) {
1163
+ if (inputRef.current.length === 0 && history.current.length === 0) {
1164
+ setTipIndex((current) => current - 1);
1165
+ return;
1166
+ }
1123
1167
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1124
1168
  if (completionCount > 0) {
1125
1169
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
@@ -1133,6 +1177,10 @@ function InkRepl({ runtime }) {
1133
1177
  return;
1134
1178
  }
1135
1179
  if (key.downArrow) {
1180
+ if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
1181
+ setTipIndex((current) => current + 1);
1182
+ return;
1183
+ }
1136
1184
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1137
1185
  if (completionCount > 0) {
1138
1186
  setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
@@ -1154,6 +1202,10 @@ function InkRepl({ runtime }) {
1154
1202
  }
1155
1203
  if (key.tab) {
1156
1204
  const currentText = inputRef.current;
1205
+ if (currentText.length === 0) {
1206
+ setTipIndex((current) => current + 1);
1207
+ return;
1208
+ }
1157
1209
  const currentCursor = cursorRef.current;
1158
1210
  const completions = slashCommandCompletions(currentText, currentCursor);
1159
1211
  const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
@@ -1165,9 +1217,10 @@ function InkRepl({ runtime }) {
1165
1217
  }
1166
1218
  if (value && !key.ctrl && !key.meta) {
1167
1219
  insertAtCursor(value);
1220
+ return;
1168
1221
  }
1169
1222
  });
1170
- 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 }), 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 }));
1223
+ 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 }), 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, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1171
1224
  }
1172
1225
  const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1173
1226
  const contentWidth = messageContentWidth(width);
@@ -1193,9 +1246,17 @@ function MessageLine({ line, width, contentWidth = messageContentWidth(width), t
1193
1246
  const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
1194
1247
  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)));
1195
1248
  }
1196
- const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, contentWidth);
1197
- const display = displayWindowForLine(line, contentWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1198
- 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)));
1249
+ const useRoleMarker = !titleProvidesToolMarker(line);
1250
+ const lineWidth = useRoleMarker ? contentWidth : toolWidth;
1251
+ const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
1252
+ const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1253
+ const contentNodes = [];
1254
+ if (line.title)
1255
+ contentNodes.push(renderBlockTitle(line));
1256
+ if (line.bodyTitle)
1257
+ contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
1258
+ contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
1259
+ return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
1199
1260
  }
1200
1261
  function displayWindowForLine(line, width, maxLines) {
1201
1262
  if (maxLines === undefined)
@@ -1265,12 +1326,21 @@ function summaryTitle(line) {
1265
1326
  function summaryUsesRoleMarker(line) {
1266
1327
  return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
1267
1328
  }
1329
+ function titleProvidesToolMarker(line) {
1330
+ return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
1331
+ }
1268
1332
  function titleStatusMarker(status) {
1269
1333
  return status === "success" ? "✓" : "✗";
1270
1334
  }
1271
1335
  function titleStatusColor(status) {
1272
1336
  return status === "success" ? "green" : "red";
1273
1337
  }
1338
+ function renderBlockTitle(line) {
1339
+ const title = line.title ?? titleForKind(line.kind);
1340
+ if (!line.titleStatus)
1341
+ return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
1342
+ 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)));
1343
+ }
1274
1344
  function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
1275
1345
  const allPreviewLines = renderSummaryLines(line, width);
1276
1346
  const preview = clipStrings(allPreviewLines, maxLines, skipTop);
@@ -1516,7 +1586,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1516
1586
  const context = renderContextParts(status.metrics);
1517
1587
  const fixedText = [
1518
1588
  phaseText,
1519
- `ctx ${context.used} / ${context.limit} (${context.percent})`,
1589
+ `ctx ${context.percent} of ${context.limit}`,
1520
1590
  `↑ ${inputValue}`,
1521
1591
  `↓ ${outputValue}`,
1522
1592
  ].join(STATUS_SEPARATOR);
@@ -1534,8 +1604,8 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1534
1604
  { text: model },
1535
1605
  statusDividerSegment(),
1536
1606
  statusLabelSegment("ctx"),
1537
- { text: ` ${context.used} / ${context.limit}` },
1538
- { text: ` (${context.percent})`, color: contextColor(status.metrics) },
1607
+ { text: ` ${context.percent}`, color: contextColor(status.metrics) },
1608
+ { text: ` of ${context.limit}` },
1539
1609
  statusDividerSegment(),
1540
1610
  statusLabelSegment("↑", tokenInputColor),
1541
1611
  { text: ` ${inputValue}` },
@@ -1681,10 +1751,16 @@ function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
1681
1751
  return undefined;
1682
1752
  return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
1683
1753
  }
1684
- function PromptLine({ text, cursor, busy, locked, placeholder = false, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
1685
- const visualLines = promptTextView(text, cursor, width, prompt);
1754
+ function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
1755
+ const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
1756
+ const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
1757
+ const visualLines = promptTextView(displayText, displayCursor, width, prompt);
1686
1758
  const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
1687
- 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 }));
1759
+ return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
1760
+ const isGhostLine = text.length === 0 && ghostText !== undefined;
1761
+ const afterColor = isGhostLine ? "gray" : inputColor;
1762
+ 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`));
1763
+ }), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
1688
1764
  }
1689
1765
  function PasteStatusLine({ text, width: terminalWidth }) {
1690
1766
  const width = statusBarWidth(terminalWidth);
@@ -1821,7 +1897,11 @@ function currentModelProvider() {
1821
1897
  return parseLoginProvider(process.env.MODEL_PROVIDER) ?? "openai";
1822
1898
  }
1823
1899
  function modelEnvKeyForProvider(provider) {
1824
- return provider === "deepseek" ? "DEEPSEEK_MODEL" : "OPENAI_MODEL";
1900
+ if (provider === "deepseek")
1901
+ return "DEEPSEEK_MODEL";
1902
+ if (provider === "kimi")
1903
+ return "KIMI_MODEL";
1904
+ return "OPENAI_MODEL";
1825
1905
  }
1826
1906
  function envValueForReasoning(reasoning) {
1827
1907
  if (reasoning === null)
@@ -2132,11 +2212,11 @@ function initialLines(runtime, lineId) {
2132
2212
  ? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
2133
2213
  : "";
2134
2214
  const lines = [
2135
- { id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}`, previewStyle: "summary" },
2215
+ { 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" },
2136
2216
  ];
2137
2217
  lineId.current = 0;
2138
2218
  if (runtime.envNotice)
2139
- lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, previewStyle: "summary" });
2219
+ lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
2140
2220
  for (const line of restoredHistoryLines(runtime))
2141
2221
  lines.push({ id: ++lineId.current, ...line });
2142
2222
  return lines;
@@ -2155,7 +2235,7 @@ function restoredHistoryLines(runtime) {
2155
2235
  }
2156
2236
  return lines;
2157
2237
  }
2158
- const LOGIN_PROVIDERS = ["openai", "deepseek"];
2238
+ const LOGIN_PROVIDERS = ["openai", "deepseek", "kimi"];
2159
2239
  const SHARED_LOGIN_FIELDS = [
2160
2240
  { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
2161
2241
  { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
@@ -2180,6 +2260,13 @@ const LOGIN_FIELD_DEFINITIONS = {
2180
2260
  { key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
2181
2261
  ...SHARED_LOGIN_FIELDS,
2182
2262
  ],
2263
+ kimi: [
2264
+ { key: "apiKey", label: "API key", envKey: "KIMI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2265
+ { key: "baseUrl", label: "Base URL", envKey: "KIMI_BASE_URL", scope: "provider", placeholder: "https://api.moonshot.cn/v1" },
2266
+ { key: "model", label: "Model", envKey: "KIMI_MODEL", scope: "provider", required: true, placeholder: "kimi-k2.6" },
2267
+ { key: "fallbackModel", label: "Fallback model", envKey: "KIMI_FALLBACK_MODEL", scope: "provider" },
2268
+ ...SHARED_LOGIN_FIELDS,
2269
+ ],
2183
2270
  };
2184
2271
  const DEPRECATED_MODEL_ENV_KEYS = [
2185
2272
  "MODEL_API_KEY",
@@ -2200,6 +2287,18 @@ const DEPRECATED_MODEL_ENV_KEYS = [
2200
2287
  "DEEPSEEK_TIMEOUT_MS",
2201
2288
  "DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
2202
2289
  "DEEPSEEK_MAX_RETRIES",
2290
+ "KIMI_REASONING_EFFORT",
2291
+ "KIMI_REASONING_SUMMARY",
2292
+ "KIMI_MAX_OUTPUT_TOKENS",
2293
+ "KIMI_TIMEOUT_MS",
2294
+ "KIMI_STREAM_IDLE_TIMEOUT_MS",
2295
+ "KIMI_MAX_RETRIES",
2296
+ "MOONSHOT_REASONING_EFFORT",
2297
+ "MOONSHOT_REASONING_SUMMARY",
2298
+ "MOONSHOT_MAX_OUTPUT_TOKENS",
2299
+ "MOONSHOT_TIMEOUT_MS",
2300
+ "MOONSHOT_STREAM_IDLE_TIMEOUT_MS",
2301
+ "MOONSHOT_MAX_RETRIES",
2203
2302
  ];
2204
2303
  function sessionsPageCount(state) {
2205
2304
  return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
@@ -2394,7 +2493,7 @@ function validateLoginForm(state) {
2394
2493
  }
2395
2494
  function createLoginFormState(envPath = getUserDotEnvPath()) {
2396
2495
  const env = parseEnvFileSafe(envPath);
2397
- const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? ((env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY) ? "deepseek" : "openai");
2496
+ const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
2398
2497
  return loginFormForProvider(currentProvider, envPath, env);
2399
2498
  }
2400
2499
  function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
@@ -2415,19 +2514,46 @@ function loginValuesForProvider(provider, env) {
2415
2514
  for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
2416
2515
  values[field.key] = env[field.envKey] ?? "";
2417
2516
  }
2517
+ if (provider === "kimi") {
2518
+ values.apiKey ||= env.MOONSHOT_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "";
2519
+ values.baseUrl ||= env.MOONSHOT_BASE_URL ?? process.env.MOONSHOT_BASE_URL ?? "";
2520
+ values.model ||= env.MOONSHOT_MODEL ?? process.env.MOONSHOT_MODEL ?? "";
2521
+ values.fallbackModel ||= env.MOONSHOT_FALLBACK_MODEL ?? process.env.MOONSHOT_FALLBACK_MODEL ?? "";
2522
+ }
2418
2523
  if (!values.baseUrl)
2419
- values.baseUrl = provider === "deepseek" ? "https://api.deepseek.com" : "https://api.openai.com";
2524
+ values.baseUrl = defaultBaseUrlForLoginProvider(provider);
2420
2525
  if (!values.model)
2421
- values.model = provider === "deepseek" ? "deepseek-chat" : "gpt-5.5";
2526
+ values.model = defaultModelForLoginProvider(provider);
2422
2527
  if (provider === "openai" && !values.endpoint)
2423
2528
  values.endpoint = "auto";
2424
2529
  return values;
2425
2530
  }
2426
2531
  function parseLoginProvider(value) {
2427
- if (value === "openai" || value === "deepseek")
2532
+ if (value === "openai" || value === "deepseek" || value === "kimi")
2428
2533
  return value;
2429
2534
  return undefined;
2430
2535
  }
2536
+ function guessLoginProvider(env) {
2537
+ if (env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY)
2538
+ return "kimi";
2539
+ if (env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY)
2540
+ return "deepseek";
2541
+ return "openai";
2542
+ }
2543
+ function defaultBaseUrlForLoginProvider(provider) {
2544
+ if (provider === "deepseek")
2545
+ return "https://api.deepseek.com";
2546
+ if (provider === "kimi")
2547
+ return "https://api.moonshot.cn/v1";
2548
+ return "https://api.openai.com";
2549
+ }
2550
+ function defaultModelForLoginProvider(provider) {
2551
+ if (provider === "deepseek")
2552
+ return "deepseek-chat";
2553
+ if (provider === "kimi")
2554
+ return "kimi-k2.6";
2555
+ return "gpt-5.5";
2556
+ }
2431
2557
  function loginFormViewHeight(state) {
2432
2558
  return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
2433
2559
  }
@@ -2444,7 +2570,7 @@ function LoginFormView({ state, width }) {
2444
2570
  const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
2445
2571
  const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
2446
2572
  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))));
2447
- }), 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_*; shared runtime fields save as MODEL_*.", contentWidth)));
2573
+ }), 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)));
2448
2574
  }
2449
2575
  function formatLoginFieldValue(field, value, cursor) {
2450
2576
  const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
@@ -2470,6 +2596,12 @@ function envEntriesForLoginForm(state) {
2470
2596
  const value = (state.values[field.key] ?? "").trim();
2471
2597
  entries[field.envKey] = value || undefined;
2472
2598
  }
2599
+ if (state.provider === "kimi") {
2600
+ entries.MOONSHOT_API_KEY = undefined;
2601
+ entries.MOONSHOT_BASE_URL = undefined;
2602
+ entries.MOONSHOT_MODEL = undefined;
2603
+ entries.MOONSHOT_FALLBACK_MODEL = undefined;
2604
+ }
2473
2605
  return entries;
2474
2606
  }
2475
2607
  function updateEnvContent(content, updates, removeKeys = []) {
@@ -2497,6 +2629,7 @@ function updateEnvContent(content, updates, removeKeys = []) {
2497
2629
  appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
2498
2630
  appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
2499
2631
  appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
2632
+ appendEnvGroup(updatedLines, "# Kimi provider settings", grouped.kimi);
2500
2633
  appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
2501
2634
  }
2502
2635
  return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
@@ -2506,6 +2639,7 @@ function groupLoginEnvEntries(entries) {
2506
2639
  active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
2507
2640
  openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
2508
2641
  deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
2642
+ kimi: entries.filter(([key]) => key.startsWith("KIMI_") || key.startsWith("MOONSHOT_")),
2509
2643
  shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
2510
2644
  };
2511
2645
  }
@@ -2639,7 +2773,7 @@ function kindForRole(role) {
2639
2773
  }
2640
2774
  function titleForKind(kind) {
2641
2775
  if (kind === "thinking")
2642
- return `${THINKING_MARKER} Think`;
2776
+ return `${THINKING_MARKER} think`;
2643
2777
  if (kind === "tool")
2644
2778
  return "Tool";
2645
2779
  if (kind === "error")
@@ -2693,6 +2827,7 @@ function formatToolUse(toolUse) {
2693
2827
  return {
2694
2828
  kind: "tool",
2695
2829
  title: toolTitle(toolUse.name, "running"),
2830
+ bodyTitle: planToolBodyTitle(toolUse.input),
2696
2831
  text: formatPlanToolPayload(toolUse.input),
2697
2832
  };
2698
2833
  }
@@ -2708,6 +2843,7 @@ function formatToolResultLine(toolName, output, ok) {
2708
2843
  const line = {
2709
2844
  kind: ok ? "tool" : "error",
2710
2845
  title: toolTitle(toolName, "finished"),
2846
+ bodyTitle: formatted.bodyTitle,
2711
2847
  titleStatus: ok ? "success" : "failure",
2712
2848
  text: formatted.text,
2713
2849
  format: formatted.format,
@@ -2749,10 +2885,12 @@ function isPlanToolPayload(value) {
2749
2885
  (item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
2750
2886
  });
2751
2887
  }
2888
+ function planToolBodyTitle(payload) {
2889
+ const title = payload.title?.trim();
2890
+ return title ? title : undefined;
2891
+ }
2752
2892
  function formatPlanToolPayload(payload) {
2753
2893
  const sections = [];
2754
- if (payload.title?.trim())
2755
- sections.push(`**${payload.title.trim()}**`);
2756
2894
  if (payload.summary?.trim())
2757
2895
  sections.push(payload.summary.trim());
2758
2896
  if (payload.note?.trim())
@@ -2877,7 +3015,7 @@ function formatToolResult(toolName, output, ok) {
2877
3015
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2878
3016
  }
2879
3017
  if (toolName === "plan" && isPlanToolPayload(output)) {
2880
- return { text: formatPlanToolPayload(output), full: true };
3018
+ return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
2881
3019
  }
2882
3020
  return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2883
3021
  }