code-ollama 0.15.1 → 0.17.0

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.
@@ -1,4 +1,4 @@
1
- import { A as LABEL, C as withSystemMessage, D as USER, E as SYSTEM, F as VERSION, I as LIST, M as SAFE, N as APPROVE, O as PLAN_GENERATION_INSTRUCTION, P as REJECT, S as resetSystemMessage, T as ASSISTANT, _ as setClearHandler, a as tick, b as loadConfig, c as appendMessage, d as deleteSessionIfEmpty, f as listSessions, g as reset, h as clear, i as WRITE_TOOLS, j as PLAN, k as AUTO, l as createSession$1, m as updateSessionModel, n as READ_TOOLS, o as color, p as loadSession, r as TOOLS, s as write, t as executeTool, u as deleteSession, v as listModels, w as HEADER_PREFIX, x as saveConfig, y as streamChat } from "../cli.js";
1
+ import { A as PLAN_GENERATION_INSTRUCTION, C as withSystemMessage, D as ASSISTANT, E as getTheme, F as APPROVE, I as REJECT, L as VERSION, M as LABEL, N as PLAN, O as SYSTEM, P as SAFE, R as LIST, S as resetSystemMessage, T as LIST$1, _ as setClearHandler, a as tick, b as loadConfig, c as appendMessage, d as deleteSessionIfEmpty, f as listSessions, g as reset, h as clear, i as WRITE_TOOLS, j as AUTO, k as USER, l as createSession, m as updateSessionModel, n as READ_TOOLS, o as color, p as loadSession, r as TOOLS, s as write, t as executeTool, u as deleteSession, v as listModels, w as HEADER_PREFIX, x as saveConfig, y as streamChat } from "../cli.js";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join, relative } from "node:path";
@@ -17,7 +17,7 @@ function normalizeCodeBlockContent(content, indent = "") {
17
17
  const indentPattern = new RegExp(`^${indent}`, "gm");
18
18
  return content.replace(indentPattern, "").trim();
19
19
  }
20
- async function prewarmCodeBlocks(content) {
20
+ async function prewarmCodeBlocks(content, theme = getTheme()) {
21
21
  const promises = [];
22
22
  let match;
23
23
  CODE_BLOCK_REGEX.lastIndex = 0;
@@ -26,35 +26,35 @@ async function prewarmCodeBlocks(content) {
26
26
  const language = match[3];
27
27
  const code = normalizeCodeBlockContent(match[4], indent);
28
28
  // v8 ignore next 2
29
- if (code) promises.push(prewarmHighlight(code, language));
29
+ if (code) promises.push(prewarmHighlight(code, language, theme));
30
30
  }
31
31
  await Promise.all(promises);
32
32
  }
33
- async function prewarmHighlight(code, language) {
33
+ async function prewarmHighlight(code, language, theme = getTheme()) {
34
34
  // v8 ignore start
35
- const cacheKey = `${language ?? ""}:${code}`;
35
+ const cacheKey = `${theme.codeTheme}:${language ?? ""}:${code}`;
36
36
  if (highlightCache.has(cacheKey)) return;
37
37
  // v8 ignore stop
38
- const result = await highlightCode(code, language);
38
+ const result = await highlightCode(code, language, theme.codeTheme);
39
39
  highlightCache.set(cacheKey, result);
40
40
  }
41
- async function highlightCode(code, language = "text") {
41
+ async function highlightCode(code, language = "text", codeTheme = getTheme().codeTheme) {
42
42
  const { codeToANSI } = await import("@shikijs/cli");
43
43
  try {
44
- return await codeToANSI(code, language, "github-light");
44
+ return await codeToANSI(code, language, codeTheme);
45
45
  } catch {
46
46
  // v8 ignore next
47
47
  return code;
48
48
  }
49
49
  }
50
- var CodeBlock = memo(function CodeBlock({ code, language, role }) {
51
- const cacheKey = `${language ?? ""}:${code}`;
50
+ var CodeBlock = memo(function CodeBlock({ code, language, role, theme = getTheme() }) {
51
+ const cacheKey = `${theme.codeTheme}:${language ?? ""}:${code}`;
52
52
  const [highlighted, setHighlighted] = useState(() => highlightCache.get(cacheKey) ?? code);
53
53
  useEffect(() => {
54
54
  let canceled = false;
55
55
  async function loadHighlight() {
56
56
  try {
57
- const result = await highlightCode(code, language);
57
+ const result = await highlightCode(code, language, theme.codeTheme);
58
58
  highlightCache.set(cacheKey, result);
59
59
  if (!canceled) setHighlighted(result);
60
60
  } catch {}
@@ -66,22 +66,31 @@ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
66
66
  }, [
67
67
  cacheKey,
68
68
  code,
69
- language
69
+ language,
70
+ theme.codeTheme
70
71
  ]);
71
72
  const isSystem = role === SYSTEM;
72
73
  return /* @__PURE__ */ jsx(Box, {
73
74
  flexDirection: "column",
74
75
  borderStyle: "bold",
75
- borderColor: isSystem ? "gray" : "dim",
76
+ borderColor: isSystem ? theme.colors.secondary : theme.colors.codeBorder,
76
77
  paddingX: 1,
77
78
  marginY: 1,
78
79
  children: /* @__PURE__ */ jsx(Text, {
80
+ color: isSystem ? theme.colors.messageSystem : void 0,
79
81
  dimColor: isSystem,
80
82
  children: highlighted
81
83
  })
82
84
  });
83
85
  });
84
86
  //#endregion
87
+ //#region src/components/Messages/constants.ts
88
+ var TURN_ABORTED_MESSAGE = [
89
+ "<turn_aborted>",
90
+ "The user interrupted the previous turn on purpose. Any running commands may still be running in the background. If any tools were aborted, they may have partially executed.",
91
+ "</turn_aborted>"
92
+ ].join("\n");
93
+ //#endregion
85
94
  //#region src/components/Markdown/extensions.ts
86
95
  var LATEX_COMMANDS = {
87
96
  "\\rightarrow": "→",
@@ -171,7 +180,7 @@ var inlineMathExtension = {
171
180
  //#endregion
172
181
  //#region src/components/Markdown/render.ts
173
182
  var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
174
- function renderMarkdown(content, hrWidth) {
183
+ function renderMarkdown(content, hrWidth, syntaxTheme = "gitHub") {
175
184
  const hr = "─".repeat(Math.max(1, hrWidth));
176
185
  const markdown = new Marked();
177
186
  const rendererExtension = {
@@ -187,7 +196,7 @@ function renderMarkdown(content, hrWidth) {
187
196
  }
188
197
  };
189
198
  markdown.use(markedTerminal({
190
- theme: "gitHub",
199
+ theme: syntaxTheme,
191
200
  reflowText: true,
192
201
  width: Math.max(1, hrWidth)
193
202
  }));
@@ -202,23 +211,20 @@ function renderMarkdown(content, hrWidth) {
202
211
  }
203
212
  //#endregion
204
213
  //#region src/components/Markdown/Markdown.tsx
205
- var Markdown = memo(function Markdown({ content, color, dimColor }) {
214
+ var Markdown = memo(function Markdown({ content, color, dimColor, theme = getTheme() }) {
206
215
  const { stdout } = useStdout();
207
216
  const availableWidth = stdout.columns - 4;
208
217
  return /* @__PURE__ */ jsx(Text, {
209
218
  color,
210
219
  dimColor,
211
- children: useMemo(() => renderMarkdown(content, availableWidth), [content, availableWidth])
220
+ children: useMemo(() => renderMarkdown(content, availableWidth, theme.markdownTheme), [
221
+ content,
222
+ availableWidth,
223
+ theme.markdownTheme
224
+ ])
212
225
  });
213
226
  });
214
227
  //#endregion
215
- //#region src/components/Messages/constants.ts
216
- var TURN_ABORTED_MESSAGE = [
217
- "<turn_aborted>",
218
- "The user interrupted the previous turn on purpose. Any running commands may still be running in the background. If any tools were aborted, they may have partially executed.",
219
- "</turn_aborted>"
220
- ].join("\n");
221
- //#endregion
222
228
  //#region src/components/Messages/layout.ts
223
229
  var ANSI_REGEX = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
224
230
  var CODE_BLOCK_MARGIN_Y = 2;
@@ -371,118 +377,31 @@ function parseContent(content) {
371
377
  return segments;
372
378
  }
373
379
  //#endregion
374
- //#region src/components/Messages/streaming.ts
375
- function isWordCharacter(char) {
376
- return char !== void 0 && /[A-Za-z0-9]/.test(char);
377
- }
378
- function isEscaped(content, index) {
379
- let slashCount = 0;
380
- for (let cursor = index - 1; cursor >= 0 && content[cursor] === "\\"; cursor--) slashCount += 1;
381
- return slashCount % 2 === 1;
382
- }
383
- function canOpenEmphasis(content, index, length) {
384
- const previous = content[index - 1];
385
- const next = content[index + length];
386
- if (!next || /\s/.test(next)) return false;
387
- return !isWordCharacter(previous);
388
- }
389
- function canCloseEmphasis(content, index, length) {
390
- const previous = content[index - 1];
391
- const next = content[index + length];
392
- if (!previous || /\s/.test(previous)) return false;
393
- return !isWordCharacter(next);
394
- }
395
- function findUnmatchedInlineDelimiter(content) {
396
- const stack = [];
397
- for (let index = 0; index < content.length; index += 1) {
398
- const current = content[index];
399
- if (isEscaped(content, index)) continue;
400
- const top = stack.at(-1);
401
- if (top?.kind === "code") {
402
- if (current === "`") stack.pop();
403
- continue;
404
- }
405
- if (top?.kind === "latex") {
406
- if (current === "$") stack.pop();
407
- continue;
408
- }
409
- if (current === "`") {
410
- stack.push({
411
- index,
412
- length: 1,
413
- kind: "code",
414
- marker: "`"
415
- });
416
- continue;
417
- }
418
- if (current === "$") {
419
- stack.push({
420
- index,
421
- length: 1,
422
- kind: "latex",
423
- marker: "$"
424
- });
425
- continue;
426
- }
427
- if (current !== "*") continue;
428
- const marker = current;
429
- const length = content[index + 1] === marker ? 2 : 1;
430
- const token = marker.repeat(length);
431
- const kind = length === 2 ? "bold" : "italic";
432
- if (top?.marker === token && top.kind === kind && canCloseEmphasis(content, index, length)) {
433
- stack.pop();
434
- if (length === 2) index += 1;
435
- continue;
436
- }
437
- if (canOpenEmphasis(content, index, length)) {
438
- stack.push({
439
- index,
440
- length,
441
- kind,
442
- marker: token
443
- });
444
- if (length === 2) index += 1;
445
- }
446
- }
447
- return stack[0] ?? null;
448
- }
449
- function splitStreamingInlineContent(content) {
450
- const unmatched = findUnmatchedInlineDelimiter(content);
451
- if (!unmatched) return [{
452
- type: "markdown",
453
- content
454
- }];
455
- const parts = [];
456
- const prefix = content.slice(0, unmatched.index);
457
- const plainSuffix = content.slice(unmatched.index + unmatched.length);
458
- if (prefix) parts.push({
459
- type: "markdown",
460
- content: prefix
461
- });
462
- if (plainSuffix) parts.push({
463
- type: "plain",
464
- content: plainSuffix
465
- });
466
- return parts;
467
- }
468
- //#endregion
469
380
  //#region src/components/Messages/styles.ts
470
- function getMessageColor(role) {
381
+ function getMessageColor(role, theme) {
471
382
  switch (role) {
472
- case USER: return "black";
473
- case ASSISTANT: return "cyan";
474
- case SYSTEM: return "gray";
383
+ case USER:
384
+ case ASSISTANT: return;
385
+ case SYSTEM: return theme.colors.messageSystem;
475
386
  default: return;
476
387
  }
477
388
  }
478
389
  //#endregion
479
- //#region src/components/Messages/Messages.tsx
480
- function Message({ message, isStreaming = false }) {
481
- const { stdout } = useStdout();
482
- const messageColor = getMessageColor(message.role);
390
+ //#region src/components/Messages/Message.tsx
391
+ function renderStickyPaddingLines(count) {
392
+ return Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx(
393
+ Text,
394
+ // v8 ignore start
395
+ { children: " " },
396
+ index
397
+ ));
398
+ }
399
+ function Message({ message, isStreaming = false, theme }) {
400
+ const messageColor = getMessageColor(message.role, theme);
483
401
  const isSystem = message.role === SYSTEM;
484
402
  const isUser = message.role === USER;
485
403
  const isStreamingAssistant = isStreaming && !isUser && !isSystem;
404
+ const { stdout } = useStdout();
486
405
  const stickyHeightRef = useRef({
487
406
  columns: stdout.columns,
488
407
  maxHeight: 0
@@ -506,7 +425,10 @@ function Message({ message, isStreaming = false }) {
506
425
  const streamingHeight = isStreamingAssistant ? segments.reduce((height, segment) => {
507
426
  if (segment.type === "code") return height + getCodeBlockHeight(segment.content, availableWidth);
508
427
  if (segment.type === "raw") return height + getCodeBlockHeight(unwrapRawMarkdownFence(segment.content) ?? segment.content, availableWidth);
509
- return height + getStreamingTextHeight(splitStreamingInlineContent(segment.content), availableWidth);
428
+ return height + getStreamingTextHeight([{
429
+ type: "markdown",
430
+ content: segment.content
431
+ }], availableWidth);
510
432
  }, 0) : 0;
511
433
  if (isStreamingAssistant) stickyHeightRef.current.maxHeight = Math.max(stickyHeightRef.current.maxHeight, streamingHeight);
512
434
  const stickyPaddingLines = isStreamingAssistant ? stickyHeightRef.current.maxHeight - streamingHeight : 0;
@@ -523,7 +445,8 @@ function Message({ message, isStreaming = false }) {
523
445
  children: /* @__PURE__ */ jsx(CodeBlock, {
524
446
  code: segment.content,
525
447
  language: segment.language,
526
- role: message.role
448
+ role: message.role,
449
+ theme
527
450
  })
528
451
  }, index);
529
452
  if (segment.type === "raw") {
@@ -533,11 +456,12 @@ function Message({ message, isStreaming = false }) {
533
456
  children: /* @__PURE__ */ jsx(CodeBlock, {
534
457
  code: markdownSource ?? segment.content,
535
458
  language: markdownSource ? "markdown" : segment.language,
536
- role: message.role
459
+ role: message.role,
460
+ theme
537
461
  })
538
462
  }, index);
539
463
  }
540
- const textParts = isStreaming && !isUser ? splitStreamingInlineContent(segment.content) : [{
464
+ const textParts = [{
541
465
  type: "markdown",
542
466
  content: segment.content
543
467
  }];
@@ -547,28 +471,31 @@ function Message({ message, isStreaming = false }) {
547
471
  }, index) : /* @__PURE__ */ jsx(Box, {
548
472
  flexDirection: "column",
549
473
  marginX: 2,
550
- children: textParts.map((part, partIndex) => part.type === "plain" ? /* @__PURE__ */ jsx(Text, {
551
- color: messageColor,
552
- children: part.content
553
- }, partIndex) : /* @__PURE__ */ jsx(Markdown, {
474
+ children: textParts.map((part, partIndex) => /* @__PURE__ */ jsx(Markdown, {
554
475
  content: part.content,
555
- color: messageColor
476
+ theme
556
477
  }, partIndex))
557
478
  }, index);
558
- }), Array.from({ length: stickyPaddingLines }, (_, index) => /* @__PURE__ */ jsx(Text, { children: " " }, "padding-" + String(index)))]
479
+ }), renderStickyPaddingLines(stickyPaddingLines)]
559
480
  });
560
481
  }
561
- function Messages({ messages, isLoading, sessionId, streamingMessage }) {
482
+ //#endregion
483
+ //#region src/components/Messages/Messages.tsx
484
+ function Messages({ messages, isLoading, sessionId, streamingMessage, theme = getTheme() }) {
562
485
  return /* @__PURE__ */ jsxs(Box, {
563
486
  flexDirection: "column",
564
487
  children: [
565
488
  /* @__PURE__ */ jsx(Static, {
566
489
  items: messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE),
567
- children: (message, index) => /* @__PURE__ */ jsx(Message, { message }, index)
490
+ children: (message, index) => /* @__PURE__ */ jsx(Message, {
491
+ message,
492
+ theme
493
+ }, index)
568
494
  }, sessionId),
569
495
  streamingMessage && /* @__PURE__ */ jsx(Message, {
570
496
  isStreaming: true,
571
- message: streamingMessage
497
+ message: streamingMessage,
498
+ theme
572
499
  }),
573
500
  isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
574
501
  marginTop: -1,
@@ -580,7 +507,7 @@ function Messages({ messages, isLoading, sessionId, streamingMessage }) {
580
507
  });
581
508
  }
582
509
  //#endregion
583
- //#region src/components/SelectPrompt.tsx
510
+ //#region src/components/SelectPrompt/SelectPrompt.tsx
584
511
  function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
585
512
  useInput((input, key) => {
586
513
  if (key.escape || key.ctrl && input === "c") onCancel?.();
@@ -591,6 +518,8 @@ function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
591
518
  children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
592
519
  });
593
520
  }
521
+ //#endregion
522
+ //#region src/components/SelectPrompt/SelectPromptHint.tsx
594
523
  function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" }) {
595
524
  return /* @__PURE__ */ jsxs(Box, {
596
525
  flexDirection: "row",
@@ -646,7 +575,7 @@ var options$1 = [
646
575
  value: PLAN
647
576
  }
648
577
  ];
649
- function PlanApproval({ planContent, onModeChange }) {
578
+ function PlanApproval({ planContent, onModeChange, theme = getTheme() }) {
650
579
  return /* @__PURE__ */ jsx(Box, {
651
580
  marginX: 2,
652
581
  children: /* @__PURE__ */ jsx(SelectPrompt, {
@@ -663,7 +592,7 @@ function PlanApproval({ planContent, onModeChange }) {
663
592
  children: [
664
593
  /* @__PURE__ */ jsx(Text, {
665
594
  bold: true,
666
- color: "magenta",
595
+ color: theme.colors.accent,
667
596
  children: "Plan Generated - Choose execution mode:"
668
597
  }),
669
598
  /* @__PURE__ */ jsx(Box, {
@@ -685,7 +614,7 @@ var options = [{
685
614
  label: "Reject tool call",
686
615
  value: REJECT
687
616
  }];
688
- function ToolApproval({ toolCall, onDecision }) {
617
+ function ToolApproval({ toolCall, onDecision, theme = getTheme() }) {
689
618
  const handleChange = useCallback((value) => {
690
619
  onDecision(value);
691
620
  }, [onDecision]);
@@ -702,7 +631,7 @@ function ToolApproval({ toolCall, onDecision }) {
702
631
  onCancel: handleEscape,
703
632
  children: [
704
633
  /* @__PURE__ */ jsx(Text, {
705
- color: "yellow",
634
+ color: theme.colors.warning,
706
635
  children: "Tool requires approval ⚠️ "
707
636
  }),
708
637
  /* @__PURE__ */ jsxs(Box, {
@@ -734,16 +663,6 @@ function ToolApproval({ toolCall, onDecision }) {
734
663
  });
735
664
  }
736
665
  //#endregion
737
- //#region src/components/Chat/constants.ts
738
- var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
739
- var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
740
- var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
741
- var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
742
- INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
743
- INTERRUPT_REASON["REJECTED"] = "rejected";
744
- return INTERRUPT_REASON;
745
- }({});
746
- //#endregion
747
666
  //#region src/components/TextInput/TextInput.tsx
748
667
  function buildLineSegments(displayValue, cursorPosition, width) {
749
668
  const safeWidth = Math.max(1, width);
@@ -1043,17 +962,28 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
1043
962
  });
1044
963
  }
1045
964
  //#endregion
1046
- //#region src/components/Chat/Input.tsx
965
+ //#region src/components/Chat/ChatInput.tsx
1047
966
  function hasFileSuggestionQuery(input) {
1048
967
  return /(^|.)@\S+/.test(input);
1049
968
  }
1050
- function Input({ isDisabled = false, onInterrupt, onSubmit }) {
969
+ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit }) {
1051
970
  const { exit } = useApp();
971
+ const [history, setHistory] = useState(sessionHistory);
972
+ const [historyIndex, setHistoryIndex] = useState(null);
1052
973
  const [input, setInput] = useState("");
1053
974
  const [cursorPosition, setCursorPosition] = useState(void 0);
1054
975
  const fileSuggestionRef = useRef(null);
976
+ useEffect(() => {
977
+ setHistory(sessionHistory);
978
+ setHistoryIndex(null);
979
+ setInput("");
980
+ setCursorPosition(void 0);
981
+ fileSuggestionRef.current = null;
982
+ }, [sessionHistory]);
1055
983
  const resetInput = useCallback(() => {
1056
984
  setInput("");
985
+ setCursorPosition(void 0);
986
+ setHistoryIndex(null);
1057
987
  }, []);
1058
988
  const handleSelectFileSuggestion = useCallback((nextInput) => {
1059
989
  setInput(nextInput.value);
@@ -1077,15 +1007,58 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1077
1007
  };
1078
1008
  } else fileSuggestionRef.current = null;
1079
1009
  }, [input]);
1010
+ const handleInputChange = useCallback((nextInput) => {
1011
+ setInput(nextInput);
1012
+ setHistoryIndex(null);
1013
+ }, []);
1080
1014
  const submitAndReset = useCallback((input) => {
1081
1015
  const trimmedInput = input.trim();
1082
1016
  if (!trimmedInput) return;
1083
1017
  onSubmit(trimmedInput);
1018
+ if (!trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
1084
1019
  resetInput();
1085
1020
  fileSuggestionRef.current = null;
1086
1021
  }, [onSubmit, resetInput]);
1087
1022
  const showCommandMenu = input.startsWith("/");
1088
1023
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
1024
+ const handleHistoryNavigation = useCallback((direction) => {
1025
+ if (!history.length || showFileSuggestions) return;
1026
+ if (direction === "up") {
1027
+ if (historyIndex === null) {
1028
+ if (input) return;
1029
+ const nextIndex = history.length - 1;
1030
+ const nextInput = history[nextIndex];
1031
+ setHistoryIndex(nextIndex);
1032
+ setInput(nextInput);
1033
+ setCursorPosition(nextInput.length);
1034
+ return;
1035
+ }
1036
+ if (historyIndex === 0) return;
1037
+ const nextIndex = historyIndex - 1;
1038
+ const nextInput = history[nextIndex];
1039
+ setHistoryIndex(nextIndex);
1040
+ setInput(nextInput);
1041
+ setCursorPosition(nextInput.length);
1042
+ return;
1043
+ }
1044
+ if (historyIndex === null) return;
1045
+ if (historyIndex === history.length - 1) {
1046
+ setHistoryIndex(null);
1047
+ setInput("");
1048
+ setCursorPosition(0);
1049
+ return;
1050
+ }
1051
+ const nextIndex = historyIndex + 1;
1052
+ const nextInput = history[nextIndex];
1053
+ setHistoryIndex(nextIndex);
1054
+ setInput(nextInput);
1055
+ setCursorPosition(nextInput.length);
1056
+ }, [
1057
+ history,
1058
+ historyIndex,
1059
+ input,
1060
+ showFileSuggestions
1061
+ ]);
1089
1062
  const handleSubmitText = useCallback((input) => {
1090
1063
  if (input.startsWith("/")) return;
1091
1064
  if (hasFileSuggestionQuery(input)) {
@@ -1097,8 +1070,8 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1097
1070
  const handleSubmitCommand = useCallback((input) => {
1098
1071
  if (LIST.find(({ name }) => name === input)) submitAndReset(input);
1099
1072
  }, [submitAndReset]);
1100
- useInput((_input, key) => {
1101
- const isCtrlC = key.ctrl && _input === "c";
1073
+ useInput((inputKey, key) => {
1074
+ const isCtrlC = key.ctrl && inputKey === "c";
1102
1075
  if (isDisabled) {
1103
1076
  if (key.escape || isCtrlC) onInterrupt?.();
1104
1077
  return;
@@ -1110,6 +1083,11 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1110
1083
  }
1111
1084
  exit();
1112
1085
  }
1086
+ if (key.upArrow) {
1087
+ handleHistoryNavigation("up");
1088
+ return;
1089
+ }
1090
+ if (key.downArrow) handleHistoryNavigation("down");
1113
1091
  });
1114
1092
  return /* @__PURE__ */ jsxs(Box, {
1115
1093
  flexDirection: "column",
@@ -1119,7 +1097,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1119
1097
  isDisabled,
1120
1098
  cursorPosition,
1121
1099
  wrapIndent: 2,
1122
- onChange: setInput,
1100
+ onChange: handleInputChange,
1123
1101
  onSubmit: handleSubmitText,
1124
1102
  placeholder: "Ask anything... (/ commands, @ files)"
1125
1103
  })] }),
@@ -1137,6 +1115,16 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1137
1115
  });
1138
1116
  }
1139
1117
  //#endregion
1118
+ //#region src/components/Chat/constants.ts
1119
+ var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1120
+ var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
1121
+ var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1122
+ var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
1123
+ INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
1124
+ INTERRUPT_REASON["REJECTED"] = "rejected";
1125
+ return INTERRUPT_REASON;
1126
+ }({});
1127
+ //#endregion
1140
1128
  //#region src/components/Chat/plan.ts
1141
1129
  function hasExecutablePlan(content) {
1142
1130
  return content.split("\n").some((line) => {
@@ -1146,8 +1134,9 @@ function hasExecutablePlan(content) {
1146
1134
  }
1147
1135
  //#endregion
1148
1136
  //#region src/components/Chat/Chat.tsx
1149
- function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
1137
+ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1150
1138
  const sessionMessages = initialMessages ?? [];
1139
+ const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
1151
1140
  const [messages, setMessages] = useState(sessionMessages);
1152
1141
  const [streamingMessage, setStreamingMessage] = useState(null);
1153
1142
  const [isLoading, setIsLoading] = useState(false);
@@ -1262,13 +1251,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1262
1251
  return;
1263
1252
  }
1264
1253
  }
1265
- await prewarmCodeBlocks(assistantMessage.content);
1254
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1266
1255
  commitAssistantMessage();
1267
1256
  } catch (error) {
1268
1257
  // v8 ignore next
1269
1258
  if (!controller.signal.aborted) {
1270
1259
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1271
- await prewarmCodeBlocks(assistantMessage.content);
1260
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1272
1261
  commitAssistantMessage();
1273
1262
  }
1274
1263
  } finally {
@@ -1278,7 +1267,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1278
1267
  }, [
1279
1268
  buildToolResultMessage,
1280
1269
  model,
1281
- mode
1270
+ mode,
1271
+ theme
1282
1272
  ]);
1283
1273
  const processStreamReadOnly = useCallback(async (currentMessages) => {
1284
1274
  const controller = new AbortController();
@@ -1334,7 +1324,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1334
1324
  return;
1335
1325
  }
1336
1326
  }
1337
- await prewarmCodeBlocks(assistantMessage.content);
1327
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1338
1328
  const researchMessages = commitAssistantMessage();
1339
1329
  const planInstruction = {
1340
1330
  role: SYSTEM,
@@ -1375,7 +1365,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1375
1365
  // v8 ignore next
1376
1366
  if (!controller.signal.aborted) {
1377
1367
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1378
- await prewarmCodeBlocks(assistantMessage.content);
1368
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1379
1369
  commitAssistantMessage();
1380
1370
  }
1381
1371
  } finally {
@@ -1385,7 +1375,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1385
1375
  }, [
1386
1376
  buildPlanModeCorrectionMessage,
1387
1377
  buildToolResultMessage,
1388
- model
1378
+ model,
1379
+ theme
1389
1380
  ]);
1390
1381
  const handlePlanApproval = useCallback(async (mode) => {
1391
1382
  // v8 ignore next
@@ -1477,26 +1468,30 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1477
1468
  messages,
1478
1469
  isLoading,
1479
1470
  sessionId,
1480
- streamingMessage
1471
+ streamingMessage,
1472
+ theme
1481
1473
  }),
1482
1474
  pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
1483
1475
  planContent: pendingPlan.planContent,
1484
- onModeChange: handlePlanApproval
1476
+ onModeChange: handlePlanApproval,
1477
+ theme
1485
1478
  }),
1486
1479
  !pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
1487
1480
  toolCall: pendingToolCall,
1488
- onDecision: handleToolApproval
1481
+ onDecision: handleToolApproval,
1482
+ theme
1489
1483
  }),
1490
1484
  interruptReason && !isLoading && /* @__PURE__ */ jsx(Box, {
1491
1485
  marginBottom: 1,
1492
1486
  children: /* @__PURE__ */ jsx(Text, {
1493
- color: "red",
1487
+ color: theme.colors.error,
1494
1488
  children: interruptReason === INTERRUPT_REASON.REJECTED ? "❗ Tool call rejected." : "❗ Execution interrupted."
1495
1489
  })
1496
1490
  }),
1497
1491
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
1498
1492
  marginTop: 1,
1499
- children: /* @__PURE__ */ jsx(Input, {
1493
+ children: /* @__PURE__ */ jsx(ChatInput, {
1494
+ history,
1500
1495
  isDisabled: isLoading,
1501
1496
  onInterrupt: handleInterrupt,
1502
1497
  onSubmit: handleSubmit
@@ -1507,29 +1502,31 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1507
1502
  }
1508
1503
  //#endregion
1509
1504
  //#region src/components/Footer.tsx
1510
- function getModeColor(mode) {
1505
+ function getModeColor(mode, theme) {
1511
1506
  switch (mode) {
1512
- case PLAN: return "blue";
1513
- case AUTO: return "red";
1514
- case SAFE: return "green";
1507
+ case PLAN: return theme.colors.modePlan;
1508
+ case AUTO: return theme.colors.modeAuto;
1509
+ case SAFE: return theme.colors.modeSafe;
1515
1510
  // v8 ignore next
1516
1511
  default: return;
1517
1512
  }
1518
1513
  }
1519
- function Footer({ mode, model, onToggleMode }) {
1514
+ function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1520
1515
  useInput((_, key) => {
1521
1516
  if (key.tab && key.shift) onToggleMode();
1522
1517
  });
1523
1518
  const modeLabel = LABEL[mode];
1519
+ const modeColor = getModeColor(mode, theme);
1524
1520
  return /* @__PURE__ */ jsx(Box, {
1525
1521
  justifyContent: "space-between",
1526
1522
  marginTop: 1,
1527
1523
  children: /* @__PURE__ */ jsxs(Text, {
1524
+ color: theme.colors.secondary,
1528
1525
  dimColor: true,
1529
1526
  children: [
1530
1527
  "Mode: ",
1531
1528
  /* @__PURE__ */ jsx(Text, {
1532
- color: getModeColor(mode),
1529
+ color: modeColor,
1533
1530
  children: modeLabel
1534
1531
  }),
1535
1532
  " (Shift+Tab to toggle)",
@@ -1537,7 +1534,7 @@ function Footer({ mode, model, onToggleMode }) {
1537
1534
  "❖",
1538
1535
  " Model: ",
1539
1536
  /* @__PURE__ */ jsx(Text, {
1540
- color: "cyan",
1537
+ color: theme.colors.model,
1541
1538
  children: model
1542
1539
  })
1543
1540
  ]
@@ -1550,7 +1547,7 @@ function abbreviatePath(dir) {
1550
1547
  const home = homedir();
1551
1548
  return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
1552
1549
  }
1553
- function Header({ model, onLoad }) {
1550
+ function Header({ model, onLoad, theme = getTheme() }) {
1554
1551
  const directory = abbreviatePath(process.cwd());
1555
1552
  useEffect(() => {
1556
1553
  onLoad();
@@ -1566,9 +1563,11 @@ function Header({ model, onLoad }) {
1566
1563
  bold: true,
1567
1564
  children: [HEADER_PREFIX, "Code Ollama"]
1568
1565
  }), /* @__PURE__ */ jsxs(Text, {
1566
+ color: theme.colors.secondary,
1569
1567
  dimColor: true,
1570
1568
  children: [
1571
- " (v",
1569
+ " ",
1570
+ "(v",
1572
1571
  VERSION,
1573
1572
  ")"
1574
1573
  ]
@@ -1577,21 +1576,24 @@ function Header({ model, onLoad }) {
1577
1576
  marginTop: 1,
1578
1577
  children: [
1579
1578
  /* @__PURE__ */ jsx(Text, {
1579
+ color: theme.colors.secondary,
1580
1580
  dimColor: true,
1581
1581
  children: "model:".padEnd(11)
1582
1582
  }),
1583
1583
  /* @__PURE__ */ jsx(Text, { children: model.padEnd(model.length + 3) }),
1584
1584
  /* @__PURE__ */ jsx(Text, {
1585
- color: "cyan",
1585
+ color: theme.colors.command,
1586
1586
  children: "/model"
1587
1587
  }),
1588
- /* @__PURE__ */ jsx(Text, {
1588
+ /* @__PURE__ */ jsxs(Text, {
1589
+ color: theme.colors.secondary,
1589
1590
  dimColor: true,
1590
- children: " to switch"
1591
+ children: [" ", "to switch"]
1591
1592
  })
1592
1593
  ]
1593
1594
  }),
1594
1595
  /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1596
+ color: theme.colors.secondary,
1595
1597
  dimColor: true,
1596
1598
  children: "directory:".padEnd(11)
1597
1599
  }), /* @__PURE__ */ jsx(Text, { children: directory })] })
@@ -1601,7 +1603,7 @@ function Header({ model, onLoad }) {
1601
1603
  }
1602
1604
  //#endregion
1603
1605
  //#region src/components/ModelPicker.tsx
1604
- function ModelPicker({ currentModel, onSelect, onClose }) {
1606
+ function ModelPicker({ currentModel, onSelect, onClose, theme = getTheme() }) {
1605
1607
  const [options, setOptions] = useState([]);
1606
1608
  const [error, setError] = useState(null);
1607
1609
  const handleChange = useCallback((model) => {
@@ -1632,7 +1634,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1632
1634
  load();
1633
1635
  }, [currentModel]);
1634
1636
  if (error) return /* @__PURE__ */ jsxs(Text, {
1635
- color: "red",
1637
+ color: theme.colors.error,
1636
1638
  children: ["Error loading models: ", error]
1637
1639
  });
1638
1640
  if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
@@ -1646,7 +1648,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1646
1648
  }
1647
1649
  //#endregion
1648
1650
  //#region src/components/SearchSettings.tsx
1649
- function SearchSettings({ currentUrl, onClose, onSave }) {
1651
+ function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
1650
1652
  const [view, setView] = useState("menu");
1651
1653
  const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1652
1654
  const [error, setError] = useState(null);
@@ -1718,10 +1720,11 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1718
1720
  placeholder: "http://localhost:8080"
1719
1721
  })] }),
1720
1722
  error && /* @__PURE__ */ jsx(Text, {
1721
- color: "red",
1723
+ color: theme.colors.error,
1722
1724
  children: error
1723
1725
  }),
1724
1726
  /* @__PURE__ */ jsx(Text, {
1727
+ color: theme.colors.secondary,
1725
1728
  dimColor: true,
1726
1729
  children: "Press Enter to save, Esc to go back."
1727
1730
  })
@@ -1732,10 +1735,14 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1732
1735
  onChange: handleChange,
1733
1736
  onCancel: onClose,
1734
1737
  children: [
1735
- /* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
1736
- color: "cyan",
1737
- children: currentUrl ?? "not set"
1738
- })] }),
1738
+ /* @__PURE__ */ jsxs(Text, { children: [
1739
+ "SearXNG URL:",
1740
+ " ",
1741
+ /* @__PURE__ */ jsx(Text, {
1742
+ color: theme.colors.status,
1743
+ children: currentUrl ?? "not set"
1744
+ })
1745
+ ] }),
1739
1746
  /* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
1740
1747
  /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
1741
1748
  ]
@@ -1762,7 +1769,7 @@ function formatSessionLabel(session, maxWidth, prefix = "") {
1762
1769
  if (availableTitleWidth < 1) return truncate(`${prefix}${session.title}${suffix}`, maxWidth);
1763
1770
  return `${prefix}${truncate(session.title, availableTitleWidth)}${suffix}`;
1764
1771
  }
1765
- function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
1772
+ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, theme = getTheme() }) {
1766
1773
  const [view, setView] = useState("main");
1767
1774
  const [error, setError] = useState();
1768
1775
  const [, refreshSessionList] = useState(0);
@@ -1848,7 +1855,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1848
1855
  error && /* @__PURE__ */ jsx(Box, {
1849
1856
  marginBottom: 1,
1850
1857
  children: /* @__PURE__ */ jsx(Text, {
1851
- color: "red",
1858
+ color: theme.colors.error,
1852
1859
  children: error
1853
1860
  })
1854
1861
  }),
@@ -1861,25 +1868,177 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1861
1868
  });
1862
1869
  }
1863
1870
  //#endregion
1864
- //#region src/components/App.tsx
1865
- function createSession(sessionId, model) {
1866
- return sessionId ? loadSession(sessionId) : createSession$1(model);
1871
+ //#region src/components/ThemeSettings.tsx
1872
+ function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
1873
+ const [selectedIndex, setSelectedIndex] = useState(() => {
1874
+ const initialIndex = LIST$1.findIndex(({ id }) => id === currentTheme);
1875
+ return initialIndex >= 0 ? initialIndex : 0;
1876
+ });
1877
+ const selectedTheme = useMemo(
1878
+ // v8 ignore next
1879
+ () => LIST$1[selectedIndex] ?? getTheme(),
1880
+ [selectedIndex]
1881
+ );
1882
+ useEffect(() => {
1883
+ onPreview(selectedTheme.id);
1884
+ }, [onPreview, selectedTheme.id]);
1885
+ useInput((input, key) => {
1886
+ if (key.escape || key.ctrl && input === "c") {
1887
+ onClose();
1888
+ return;
1889
+ }
1890
+ if (key.upArrow) {
1891
+ setSelectedIndex((current) => current === 0 ? LIST$1.length - 1 : current - 1);
1892
+ return;
1893
+ }
1894
+ if (key.downArrow) {
1895
+ setSelectedIndex((current) => current === LIST$1.length - 1 ? 0 : current + 1);
1896
+ return;
1897
+ }
1898
+ if (key.return) onSave(selectedTheme.id);
1899
+ });
1900
+ return /* @__PURE__ */ jsxs(Box, {
1901
+ flexDirection: "column",
1902
+ children: [
1903
+ /* @__PURE__ */ jsxs(Text, { children: [
1904
+ "Theme:",
1905
+ " ",
1906
+ /* @__PURE__ */ jsx(Text, {
1907
+ color: selectedTheme.colors.accent,
1908
+ children: selectedTheme.label
1909
+ })
1910
+ ] }),
1911
+ /* @__PURE__ */ jsx(Text, {
1912
+ color: selectedTheme.colors.secondary,
1913
+ children: selectedTheme.description
1914
+ }),
1915
+ /* @__PURE__ */ jsx(Box, {
1916
+ flexDirection: "column",
1917
+ marginTop: 1,
1918
+ children: LIST$1.map((theme, index) => {
1919
+ const isSelected = index === selectedIndex;
1920
+ return /* @__PURE__ */ jsxs(Text, {
1921
+ color: isSelected ? selectedTheme.colors.accent : void 0,
1922
+ children: [
1923
+ isSelected ? "›" : " ",
1924
+ " ",
1925
+ theme.label
1926
+ ]
1927
+ }, theme.id);
1928
+ })
1929
+ }),
1930
+ /* @__PURE__ */ jsxs(Box, {
1931
+ borderColor: selectedTheme.colors.border,
1932
+ borderStyle: "round",
1933
+ flexDirection: "column",
1934
+ marginTop: 1,
1935
+ paddingX: 1,
1936
+ children: [
1937
+ /* @__PURE__ */ jsxs(Text, {
1938
+ color: selectedTheme.colors.status,
1939
+ children: [HEADER_PREFIX, " Preview"]
1940
+ }),
1941
+ /* @__PURE__ */ jsx(Text, {
1942
+ color: selectedTheme.colors.secondary,
1943
+ children: "Markdown and code styling follow the selected theme."
1944
+ }),
1945
+ /* @__PURE__ */ jsxs(Text, { children: [
1946
+ "Status accent:",
1947
+ " ",
1948
+ /* @__PURE__ */ jsx(Text, {
1949
+ color: selectedTheme.colors.status,
1950
+ children: "search enabled"
1951
+ })
1952
+ ] }),
1953
+ /* @__PURE__ */ jsx(CodeBlock, {
1954
+ code: "const theme = 'preview';",
1955
+ language: "ts",
1956
+ role: "assistant",
1957
+ theme: selectedTheme
1958
+ })
1959
+ ]
1960
+ }),
1961
+ /* @__PURE__ */ jsx(Box, {
1962
+ marginTop: 1,
1963
+ children: /* @__PURE__ */ jsx(SelectPromptHint, {
1964
+ message: "Preview theme",
1965
+ escapeLabel: "cancel and restore"
1966
+ })
1967
+ })
1968
+ ]
1969
+ });
1867
1970
  }
1868
- function App({ sessionId }) {
1971
+ //#endregion
1972
+ //#region src/components/App/constants.ts
1973
+ var SCREEN = /* @__PURE__ */ function(SCREEN) {
1974
+ SCREEN["CHAT"] = "chat";
1975
+ SCREEN["MODEL_PICKER"] = "model-picker";
1976
+ SCREEN["SEARCH_SETTINGS"] = "search-settings";
1977
+ SCREEN["SESSION_MANAGER"] = "session-manager";
1978
+ SCREEN["THEME_SETTINGS"] = "theme-settings";
1979
+ return SCREEN;
1980
+ }({});
1981
+ //#endregion
1982
+ //#region src/components/App/hooks/useScreenRouter.ts
1983
+ function useScreenRouter() {
1869
1984
  const { exit } = useApp();
1870
- const [appConfig, setConfig] = useState(() => loadConfig());
1871
- const [currentScreen, setScreen] = useState("chat");
1872
- const [mode, setMode] = useState(SAFE);
1873
- const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
1874
- const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
1985
+ const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1986
+ return {
1987
+ currentScreen,
1988
+ setScreen,
1989
+ handleClose: useCallback(() => {
1990
+ setScreen(SCREEN.CHAT);
1991
+ }, []),
1992
+ handleCommand: useCallback((command, callbacks) => {
1993
+ const { onCreateSession, onSetPreviewThemeId, model, theme } = callbacks;
1994
+ switch (command) {
1995
+ case "/session":
1996
+ setScreen(SCREEN.SESSION_MANAGER);
1997
+ break;
1998
+ case "/model":
1999
+ setScreen(SCREEN.MODEL_PICKER);
2000
+ break;
2001
+ case "/search":
2002
+ setScreen(SCREEN.SEARCH_SETTINGS);
2003
+ break;
2004
+ case "/theme":
2005
+ onSetPreviewThemeId(theme);
2006
+ setScreen(SCREEN.THEME_SETTINGS);
2007
+ break;
2008
+ case "/clear": {
2009
+ resetSystemMessage();
2010
+ const nextSession = onCreateSession(model);
2011
+ setScreen(SCREEN.CHAT);
2012
+ clear(nextSession.metadata.id);
2013
+ break;
2014
+ }
2015
+ case "/exit":
2016
+ exit();
2017
+ break;
2018
+ }
2019
+ }, [exit])
2020
+ };
2021
+ }
2022
+ //#endregion
2023
+ //#region src/components/App/hooks/useSessionManager.ts
2024
+ function useSessionManager({ sessionId, model, commandColor }) {
2025
+ const [activeSession, setSession] = useState(() => sessionId ? loadSession(sessionId) : createSession(model));
1875
2026
  const sessionRef = useRef(activeSession);
2027
+ const commandColorRef = useRef(commandColor);
2028
+ const modelRef = useRef(model);
1876
2029
  useEffect(() => {
1877
2030
  sessionRef.current = activeSession;
1878
2031
  }, [activeSession]);
2032
+ useEffect(() => {
2033
+ commandColorRef.current = commandColor;
2034
+ }, [commandColor]);
2035
+ useEffect(() => {
2036
+ modelRef.current = model;
2037
+ }, [model]);
1879
2038
  useEffect(() => {
1880
2039
  return () => {
1881
2040
  const currentSession = sessionRef.current;
1882
- if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, "cyan")}\n`);
2041
+ if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, commandColorRef.current)}\n`);
1883
2042
  };
1884
2043
  }, []);
1885
2044
  const setActiveSession = useCallback((nextSession) => {
@@ -1888,73 +2047,81 @@ function App({ sessionId }) {
1888
2047
  return nextSession;
1889
2048
  });
1890
2049
  }, []);
1891
- const handleHeaderLoad = useCallback(() => {
1892
- setIsHeaderLoaded(true);
2050
+ return {
2051
+ activeSession,
2052
+ sessionRef,
2053
+ setActiveSession,
2054
+ setSession,
2055
+ handleCreateSession: useCallback(() => {
2056
+ const nextSession = createSession(modelRef.current);
2057
+ setActiveSession(nextSession);
2058
+ clear(nextSession.metadata.id);
2059
+ return nextSession;
2060
+ }, [setActiveSession]),
2061
+ handleOpenSession: useCallback((sessionId) => {
2062
+ if (sessionRef.current.metadata.id === sessionId) return false;
2063
+ setActiveSession(loadSession(sessionId));
2064
+ clear(sessionId);
2065
+ return true;
2066
+ }, [setActiveSession]),
2067
+ handleDeleteSession: useCallback((sessionId) => {
2068
+ deleteSession(sessionId);
2069
+ setSession((current) => {
2070
+ if (current.metadata.id !== sessionId) return current;
2071
+ return createSession(modelRef.current);
2072
+ });
2073
+ }, []),
2074
+ handleMessagesChange: useCallback((messages) => {
2075
+ setSession((current) => {
2076
+ const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
2077
+ if (persistedMessages.length <= current.messages.length) return current;
2078
+ let metadata = current.metadata;
2079
+ for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, modelRef.current);
2080
+ return {
2081
+ metadata,
2082
+ messages: persistedMessages
2083
+ };
2084
+ });
2085
+ }, [])
2086
+ };
2087
+ }
2088
+ //#endregion
2089
+ //#region src/components/App/hooks/useThemeSettings.ts
2090
+ function useThemeSettings({ currentTheme, onUpdateConfig, setScreen }) {
2091
+ const [previewThemeId, setPreviewThemeId] = useState(null);
2092
+ const activeThemeId = previewThemeId ?? currentTheme;
2093
+ const activeTheme = getTheme(activeThemeId);
2094
+ const handleThemePreview = useCallback((themeId) => {
2095
+ setPreviewThemeId(themeId);
1893
2096
  }, []);
1894
- const handleCreateSession = useCallback(() => {
1895
- const nextSession = createSession$1(appConfig.model);
1896
- setActiveSession(nextSession);
1897
- setScreen("chat");
1898
- clear(nextSession.metadata.id);
1899
- return nextSession;
1900
- }, [appConfig.model, setActiveSession]);
1901
- const handleOpenSession = useCallback((sessionId) => {
1902
- if (sessionRef.current.metadata.id === sessionId) {
1903
- setScreen("chat");
1904
- return;
1905
- }
1906
- setActiveSession(loadSession(sessionId));
1907
- setScreen("chat");
1908
- clear(sessionId);
1909
- }, [setActiveSession]);
1910
- const handleDeleteSession = useCallback((sessionId) => {
1911
- deleteSession(sessionId);
1912
- setSession((current) => {
1913
- if (current.metadata.id !== sessionId) return current;
1914
- return createSession$1(appConfig.model);
1915
- });
1916
- setScreen("session-manager");
1917
- }, [appConfig.model]);
1918
- const handleMessagesChange = useCallback((messages) => {
1919
- setSession((current) => {
1920
- const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
1921
- if (persistedMessages.length <= current.messages.length) return current;
1922
- let metadata = current.metadata;
1923
- for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, appConfig.model);
1924
- return {
1925
- metadata,
1926
- messages: persistedMessages
1927
- };
1928
- });
1929
- }, [appConfig.model]);
1930
- const handleCommand = useCallback((command) => {
1931
- switch (command) {
1932
- case "/session":
1933
- setScreen("session-manager");
1934
- break;
1935
- case "/model":
1936
- setScreen("model-picker");
1937
- break;
1938
- case "/search":
1939
- setScreen("search-settings");
1940
- break;
1941
- case "/clear": {
1942
- resetSystemMessage();
1943
- setScreen("chat");
1944
- const nextSession = createSession$1(appConfig.model);
1945
- setActiveSession(nextSession);
1946
- clear(nextSession.metadata.id);
1947
- break;
1948
- }
1949
- case "/exit":
1950
- exit();
1951
- break;
1952
- }
1953
- }, [
1954
- appConfig.model,
1955
- exit,
1956
- setActiveSession
1957
- ]);
2097
+ return {
2098
+ activeTheme,
2099
+ activeThemeId,
2100
+ handleThemeClose: useCallback(() => {
2101
+ setPreviewThemeId(null);
2102
+ setScreen(SCREEN.CHAT);
2103
+ }, [setScreen]),
2104
+ handleThemePreview,
2105
+ handleThemeSave: useCallback((themeId) => {
2106
+ setPreviewThemeId(null);
2107
+ onUpdateConfig({ theme: themeId });
2108
+ }, [onUpdateConfig]),
2109
+ previewThemeId,
2110
+ setPreviewThemeId
2111
+ };
2112
+ }
2113
+ //#endregion
2114
+ //#region src/components/App/App.tsx
2115
+ function App({ sessionId }) {
2116
+ const [appConfig, setConfig] = useState(() => loadConfig());
2117
+ const [mode, setMode] = useState(SAFE);
2118
+ const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
2119
+ const { currentScreen, setScreen, handleClose, handleCommand } = useScreenRouter();
2120
+ const { activeSession, setSession, handleCreateSession, handleOpenSession, handleDeleteSession, handleMessagesChange } = useSessionManager({
2121
+ sessionId,
2122
+ model: appConfig.model,
2123
+ commandColor: getTheme(appConfig.theme).colors.command
2124
+ });
1958
2125
  const handleUpdateConfig = useCallback((update) => {
1959
2126
  setConfig((current) => ({
1960
2127
  ...current,
@@ -1966,10 +2133,15 @@ function App({ sessionId }) {
1966
2133
  ...current,
1967
2134
  metadata: updateSessionModel(current.metadata.id, newModel)
1968
2135
  }));
1969
- setScreen("chat");
1970
- }, []);
1971
- const handleClose = useCallback(() => {
1972
- setScreen("chat");
2136
+ setScreen(SCREEN.CHAT);
2137
+ }, [setScreen, setSession]);
2138
+ const { activeTheme, handleThemeClose, handleThemePreview, handleThemeSave, setPreviewThemeId } = useThemeSettings({
2139
+ currentTheme: appConfig.theme,
2140
+ onUpdateConfig: handleUpdateConfig,
2141
+ setScreen
2142
+ });
2143
+ const handleHeaderLoad = useCallback(() => {
2144
+ setIsHeaderLoaded(true);
1973
2145
  }, []);
1974
2146
  const handleToggleMode = useCallback(() => {
1975
2147
  setMode((mode) => {
@@ -1981,40 +2153,78 @@ function App({ sessionId }) {
1981
2153
  }
1982
2154
  });
1983
2155
  }, []);
2156
+ const handleChatCommand = useCallback((command) => {
2157
+ handleCommand(command, {
2158
+ model: appConfig.model,
2159
+ theme: appConfig.theme,
2160
+ onCreateSession: handleCreateSession,
2161
+ onSetPreviewThemeId: setPreviewThemeId
2162
+ });
2163
+ }, [
2164
+ appConfig.model,
2165
+ appConfig.theme,
2166
+ handleCommand,
2167
+ handleCreateSession,
2168
+ setPreviewThemeId
2169
+ ]);
2170
+ const handleDeleteSessionAndStay = useCallback((sid) => {
2171
+ handleDeleteSession(sid);
2172
+ setScreen(SCREEN.SESSION_MANAGER);
2173
+ }, [handleDeleteSession, setScreen]);
2174
+ const handleOpenSessionAndNavigate = useCallback((sid) => {
2175
+ handleOpenSession(sid);
2176
+ setScreen(SCREEN.CHAT);
2177
+ }, [handleOpenSession, setScreen]);
2178
+ const handleCreateSessionAndNavigate = useCallback(() => {
2179
+ handleCreateSession();
2180
+ setScreen(SCREEN.CHAT);
2181
+ }, [handleCreateSession, setScreen]);
1984
2182
  let screenContent;
1985
2183
  switch (currentScreen) {
1986
- case "model-picker":
2184
+ case SCREEN.MODEL_PICKER:
1987
2185
  screenContent = /* @__PURE__ */ jsx(ModelPicker, {
1988
2186
  currentModel: appConfig.model,
1989
2187
  onSelect: handleUpdateConfig,
1990
- onClose: handleClose
2188
+ onClose: handleClose,
2189
+ theme: activeTheme
1991
2190
  });
1992
2191
  break;
1993
- case "search-settings":
2192
+ case SCREEN.SEARCH_SETTINGS:
1994
2193
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
1995
2194
  currentUrl: appConfig.searxngBaseUrl,
1996
2195
  onSave: handleUpdateConfig,
1997
- onClose: handleClose
2196
+ onClose: handleClose,
2197
+ theme: activeTheme
1998
2198
  });
1999
2199
  break;
2000
- case "session-manager":
2200
+ case SCREEN.SESSION_MANAGER:
2001
2201
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
2002
2202
  currentSessionId: activeSession.metadata.id,
2003
2203
  onClose: handleClose,
2004
- onDelete: handleDeleteSession,
2005
- onNew: handleCreateSession,
2006
- onOpen: handleOpenSession
2204
+ onDelete: handleDeleteSessionAndStay,
2205
+ onNew: handleCreateSessionAndNavigate,
2206
+ onOpen: handleOpenSessionAndNavigate,
2207
+ theme: activeTheme
2208
+ });
2209
+ break;
2210
+ case SCREEN.THEME_SETTINGS:
2211
+ screenContent = /* @__PURE__ */ jsx(ThemeSettings, {
2212
+ currentTheme: appConfig.theme,
2213
+ onClose: handleThemeClose,
2214
+ onPreview: handleThemePreview,
2215
+ onSave: handleThemeSave
2007
2216
  });
2008
2217
  break;
2009
- case "chat":
2218
+ case SCREEN.CHAT:
2010
2219
  screenContent = /* @__PURE__ */ jsx(Chat, {
2011
2220
  initialMessages: activeSession.messages,
2012
2221
  model: appConfig.model,
2013
- onCommand: handleCommand,
2222
+ onCommand: handleChatCommand,
2014
2223
  onMessagesChange: handleMessagesChange,
2015
2224
  mode,
2016
2225
  onModeChange: setMode,
2017
- sessionId: activeSession.metadata.id
2226
+ sessionId: activeSession.metadata.id,
2227
+ theme: activeTheme
2018
2228
  });
2019
2229
  break;
2020
2230
  }
@@ -2023,13 +2233,15 @@ function App({ sessionId }) {
2023
2233
  children: [
2024
2234
  /* @__PURE__ */ jsx(Header, {
2025
2235
  model: appConfig.model,
2026
- onLoad: handleHeaderLoad
2236
+ onLoad: handleHeaderLoad,
2237
+ theme: activeTheme
2027
2238
  }),
2028
2239
  isHeaderLoaded && screenContent,
2029
2240
  /* @__PURE__ */ jsx(Footer, {
2030
2241
  mode,
2031
2242
  model: appConfig.model,
2032
- onToggleMode: handleToggleMode
2243
+ onToggleMode: handleToggleMode,
2244
+ theme: activeTheme
2033
2245
  })
2034
2246
  ]
2035
2247
  });