code-ollama 0.16.0 → 0.18.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 SYSTEM, B as REJECT, C as resetSystemMessage, D as LIST$1, E as WARNING, F as AUTO, H as LIST, I as LABEL, L as PLAN, M as PLAN_GENERATION_INSTRUCTION, N as BACK, O as getTheme, P as CATALOG, R as SAFE, S as saveConfig, T as HEADER_PREFIX, V as VERSION, _ as deleteModel, a as color, b as streamChat, c as createSession, d as listSessions, f as loadSession, g as setClearHandler, h as reset, i as WRITE_TOOLS, j as USER, k as ASSISTANT, l as deleteSession, m as clear, n as READ_TOOLS, o as write, p as updateSessionModel, r as TOOLS, s as appendMessage, t as executeTool, u as deleteSessionIfEmpty, v as listModels, w as withSystemMessage, x as loadConfig, y as pullModel, z as APPROVE } 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";
@@ -6,7 +6,7 @@ import { exec } from "node:child_process";
6
6
  import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
7
7
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
- import { Select, Spinner } from "@inkjs/ui";
9
+ import { ProgressBar, Select, Spinner } from "@inkjs/ui";
10
10
  import { Marked } from "marked";
11
11
  import { markedTerminal } from "marked-terminal";
12
12
  //#region src/components/CodeBlock/CodeBlock.tsx
@@ -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]);
@@ -701,9 +630,13 @@ function ToolApproval({ toolCall, onDecision }) {
701
630
  onChange: handleChange,
702
631
  onCancel: handleEscape,
703
632
  children: [
704
- /* @__PURE__ */ jsx(Text, {
705
- color: "yellow",
706
- children: "Tool requires approval ⚠️ "
633
+ /* @__PURE__ */ jsxs(Text, {
634
+ color: theme.colors.warning,
635
+ children: [
636
+ "Tool requires approval ",
637
+ WARNING,
638
+ " "
639
+ ]
707
640
  }),
708
641
  /* @__PURE__ */ jsxs(Box, {
709
642
  flexDirection: "column",
@@ -888,8 +821,57 @@ function CommandMenu({ input, onSubmit }) {
888
821
  });
889
822
  }
890
823
  //#endregion
824
+ //#region src/components/Suggestions.tsx
825
+ var DEFAULT_MAX_VISIBLE_OPTIONS = 5;
826
+ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_MAX_VISIBLE_OPTIONS, resetKey, onHighlight, onSelect }) {
827
+ const [focusedIndex, setFocusedIndex] = useState(0);
828
+ useEffect(() => {
829
+ setFocusedIndex(0);
830
+ }, [resetKey]);
831
+ useEffect(() => {
832
+ if (!options.length) {
833
+ setFocusedIndex(0);
834
+ onHighlight?.(null);
835
+ return;
836
+ }
837
+ setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
838
+ }, [onHighlight, options]);
839
+ useEffect(() => {
840
+ onHighlight?.(options[focusedIndex] ?? null);
841
+ }, [
842
+ focusedIndex,
843
+ onHighlight,
844
+ options
845
+ ]);
846
+ useInput((_, key) => {
847
+ if (isDisabled || !options.length) return;
848
+ if (key.downArrow) {
849
+ setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
850
+ return;
851
+ }
852
+ if (key.upArrow) {
853
+ setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
854
+ return;
855
+ }
856
+ if (key.tab || key.return) onSelect(options[focusedIndex]);
857
+ });
858
+ if (!options.length) return null;
859
+ const visibleStart = Math.min(Math.max(0, focusedIndex - maxVisibleOptions + 1), Math.max(0, options.length - maxVisibleOptions));
860
+ return /* @__PURE__ */ jsx(Box, {
861
+ flexDirection: "column",
862
+ children: options.slice(visibleStart, visibleStart + maxVisibleOptions).map((option, index) => {
863
+ return /* @__PURE__ */ jsx(Box, {
864
+ marginLeft: 2,
865
+ children: /* @__PURE__ */ jsx(Text, {
866
+ color: visibleStart + index === focusedIndex ? "cyan" : void 0,
867
+ children: option.label
868
+ })
869
+ }, option.label);
870
+ })
871
+ });
872
+ }
873
+ //#endregion
891
874
  //#region src/components/Chat/FileSuggestions.tsx
892
- var MAX_VISIBLE_OPTIONS = 5;
893
875
  var MENTION_PATTERN = /(^|.)@(\S+)/;
894
876
  var RIPGREP_MAX_BUFFER = 10 * 1024 * 1024;
895
877
  function normalizePath(filePath) {
@@ -968,7 +950,6 @@ async function listProjectFiles(rootDir) {
968
950
  }
969
951
  function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
970
952
  const [filePaths, setFilePaths] = useState([]);
971
- const [focusedIndex, setFocusedIndex] = useState(0);
972
953
  useEffect(() => {
973
954
  async function loadProjectFiles() {
974
955
  setFilePaths(await listProjectFiles(process.cwd()));
@@ -981,55 +962,29 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
981
962
  const normalizedQuery = mentionMatch.query.toLowerCase();
982
963
  return filePaths.filter((filePath) => filePath.toLowerCase().includes(normalizedQuery));
983
964
  }, [filePaths, mentionMatch]);
984
- useEffect(() => {
985
- setFocusedIndex(0);
986
- }, [input]);
987
- useEffect(() => {
988
- if (!options.length) {
989
- setFocusedIndex(0);
990
- return;
991
- }
992
- setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
993
- }, [options]);
994
965
  useEffect(() => {
995
966
  if (!onChange) return;
996
- if (!mentionMatch || !options.length) {
997
- onChange(null);
998
- return;
999
- }
1000
- onChange(buildNextInput(input, options[focusedIndex]).value);
967
+ if (!mentionMatch || !options.length) onChange(null);
1001
968
  }, [
1002
- focusedIndex,
1003
- input,
1004
969
  mentionMatch,
1005
970
  onChange,
1006
971
  options
1007
972
  ]);
1008
- useInput((_, key) => {
1009
- if (isDisabled || !options.length) return;
1010
- if (key.downArrow) {
1011
- setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
1012
- return;
1013
- }
1014
- if (key.upArrow) {
1015
- setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
1016
- return;
1017
- }
1018
- if (key.tab || key.return) onSelect(buildNextInput(input, options[focusedIndex]));
1019
- });
1020
973
  if (!mentionMatch || !options.length) return null;
1021
- const visibleStart = Math.min(Math.max(0, focusedIndex - MAX_VISIBLE_OPTIONS + 1), Math.max(0, options.length - MAX_VISIBLE_OPTIONS));
1022
- return /* @__PURE__ */ jsx(Box, {
1023
- flexDirection: "column",
1024
- children: options.slice(visibleStart, visibleStart + MAX_VISIBLE_OPTIONS).map((option, index) => {
1025
- return /* @__PURE__ */ jsx(Box, {
1026
- marginLeft: 2,
1027
- children: /* @__PURE__ */ jsx(Text, {
1028
- color: visibleStart + index === focusedIndex ? "cyan" : void 0,
1029
- children: option
1030
- })
1031
- }, option);
1032
- })
974
+ return /* @__PURE__ */ jsx(Suggestions, {
975
+ isDisabled,
976
+ options: options.map((option) => ({
977
+ label: option,
978
+ value: option
979
+ })),
980
+ resetKey: input,
981
+ onHighlight: (option) => {
982
+ // v8 ignore next
983
+ onChange?.(option ? buildNextInput(input, option.value).value : null);
984
+ },
985
+ onSelect: (option) => {
986
+ onSelect(buildNextInput(input, option.value));
987
+ }
1033
988
  });
1034
989
  }
1035
990
  //#endregion
@@ -1205,7 +1160,7 @@ function hasExecutablePlan(content) {
1205
1160
  }
1206
1161
  //#endregion
1207
1162
  //#region src/components/Chat/Chat.tsx
1208
- function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
1163
+ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1209
1164
  const sessionMessages = initialMessages ?? [];
1210
1165
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
1211
1166
  const [messages, setMessages] = useState(sessionMessages);
@@ -1322,13 +1277,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1322
1277
  return;
1323
1278
  }
1324
1279
  }
1325
- await prewarmCodeBlocks(assistantMessage.content);
1280
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1326
1281
  commitAssistantMessage();
1327
1282
  } catch (error) {
1328
1283
  // v8 ignore next
1329
1284
  if (!controller.signal.aborted) {
1330
1285
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1331
- await prewarmCodeBlocks(assistantMessage.content);
1286
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1332
1287
  commitAssistantMessage();
1333
1288
  }
1334
1289
  } finally {
@@ -1338,7 +1293,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1338
1293
  }, [
1339
1294
  buildToolResultMessage,
1340
1295
  model,
1341
- mode
1296
+ mode,
1297
+ theme
1342
1298
  ]);
1343
1299
  const processStreamReadOnly = useCallback(async (currentMessages) => {
1344
1300
  const controller = new AbortController();
@@ -1394,7 +1350,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1394
1350
  return;
1395
1351
  }
1396
1352
  }
1397
- await prewarmCodeBlocks(assistantMessage.content);
1353
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1398
1354
  const researchMessages = commitAssistantMessage();
1399
1355
  const planInstruction = {
1400
1356
  role: SYSTEM,
@@ -1435,7 +1391,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1435
1391
  // v8 ignore next
1436
1392
  if (!controller.signal.aborted) {
1437
1393
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1438
- await prewarmCodeBlocks(assistantMessage.content);
1394
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1439
1395
  commitAssistantMessage();
1440
1396
  }
1441
1397
  } finally {
@@ -1445,7 +1401,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1445
1401
  }, [
1446
1402
  buildPlanModeCorrectionMessage,
1447
1403
  buildToolResultMessage,
1448
- model
1404
+ model,
1405
+ theme
1449
1406
  ]);
1450
1407
  const handlePlanApproval = useCallback(async (mode) => {
1451
1408
  // v8 ignore next
@@ -1537,21 +1494,24 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1537
1494
  messages,
1538
1495
  isLoading,
1539
1496
  sessionId,
1540
- streamingMessage
1497
+ streamingMessage,
1498
+ theme
1541
1499
  }),
1542
1500
  pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
1543
1501
  planContent: pendingPlan.planContent,
1544
- onModeChange: handlePlanApproval
1502
+ onModeChange: handlePlanApproval,
1503
+ theme
1545
1504
  }),
1546
1505
  !pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
1547
1506
  toolCall: pendingToolCall,
1548
- onDecision: handleToolApproval
1507
+ onDecision: handleToolApproval,
1508
+ theme
1549
1509
  }),
1550
1510
  interruptReason && !isLoading && /* @__PURE__ */ jsx(Box, {
1551
1511
  marginBottom: 1,
1552
1512
  children: /* @__PURE__ */ jsx(Text, {
1553
- color: "red",
1554
- children: interruptReason === INTERRUPT_REASON.REJECTED ? "❗ Tool call rejected." : "❗ Execution interrupted."
1513
+ color: theme.colors.error,
1514
+ children: interruptReason === INTERRUPT_REASON.REJECTED ? `❗ Tool call rejected.` : `❗ Execution interrupted.`
1555
1515
  })
1556
1516
  }),
1557
1517
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
@@ -1568,29 +1528,31 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1568
1528
  }
1569
1529
  //#endregion
1570
1530
  //#region src/components/Footer.tsx
1571
- function getModeColor(mode) {
1531
+ function getModeColor(mode, theme) {
1572
1532
  switch (mode) {
1573
- case PLAN: return "blue";
1574
- case AUTO: return "red";
1575
- case SAFE: return "green";
1533
+ case PLAN: return theme.colors.modePlan;
1534
+ case AUTO: return theme.colors.modeAuto;
1535
+ case SAFE: return theme.colors.modeSafe;
1576
1536
  // v8 ignore next
1577
1537
  default: return;
1578
1538
  }
1579
1539
  }
1580
- function Footer({ mode, model, onToggleMode }) {
1540
+ function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1581
1541
  useInput((_, key) => {
1582
1542
  if (key.tab && key.shift) onToggleMode();
1583
1543
  });
1584
1544
  const modeLabel = LABEL[mode];
1545
+ const modeColor = getModeColor(mode, theme);
1585
1546
  return /* @__PURE__ */ jsx(Box, {
1586
1547
  justifyContent: "space-between",
1587
1548
  marginTop: 1,
1588
1549
  children: /* @__PURE__ */ jsxs(Text, {
1550
+ color: theme.colors.secondary,
1589
1551
  dimColor: true,
1590
1552
  children: [
1591
1553
  "Mode: ",
1592
1554
  /* @__PURE__ */ jsx(Text, {
1593
- color: getModeColor(mode),
1555
+ color: modeColor,
1594
1556
  children: modeLabel
1595
1557
  }),
1596
1558
  " (Shift+Tab to toggle)",
@@ -1598,7 +1560,7 @@ function Footer({ mode, model, onToggleMode }) {
1598
1560
  "❖",
1599
1561
  " Model: ",
1600
1562
  /* @__PURE__ */ jsx(Text, {
1601
- color: "cyan",
1563
+ color: theme.colors.model,
1602
1564
  children: model
1603
1565
  })
1604
1566
  ]
@@ -1611,7 +1573,7 @@ function abbreviatePath(dir) {
1611
1573
  const home = homedir();
1612
1574
  return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
1613
1575
  }
1614
- function Header({ model, onLoad }) {
1576
+ function Header({ model, onLoad, theme = getTheme() }) {
1615
1577
  const directory = abbreviatePath(process.cwd());
1616
1578
  useEffect(() => {
1617
1579
  onLoad();
@@ -1627,9 +1589,11 @@ function Header({ model, onLoad }) {
1627
1589
  bold: true,
1628
1590
  children: [HEADER_PREFIX, "Code Ollama"]
1629
1591
  }), /* @__PURE__ */ jsxs(Text, {
1592
+ color: theme.colors.secondary,
1630
1593
  dimColor: true,
1631
1594
  children: [
1632
- " (v",
1595
+ " ",
1596
+ "(v",
1633
1597
  VERSION,
1634
1598
  ")"
1635
1599
  ]
@@ -1638,21 +1602,24 @@ function Header({ model, onLoad }) {
1638
1602
  marginTop: 1,
1639
1603
  children: [
1640
1604
  /* @__PURE__ */ jsx(Text, {
1605
+ color: theme.colors.secondary,
1641
1606
  dimColor: true,
1642
1607
  children: "model:".padEnd(11)
1643
1608
  }),
1644
1609
  /* @__PURE__ */ jsx(Text, { children: model.padEnd(model.length + 3) }),
1645
1610
  /* @__PURE__ */ jsx(Text, {
1646
- color: "cyan",
1611
+ color: theme.colors.command,
1647
1612
  children: "/model"
1648
1613
  }),
1649
- /* @__PURE__ */ jsx(Text, {
1614
+ /* @__PURE__ */ jsxs(Text, {
1615
+ color: theme.colors.secondary,
1650
1616
  dimColor: true,
1651
- children: " to switch"
1617
+ children: [" ", "to manage"]
1652
1618
  })
1653
1619
  ]
1654
1620
  }),
1655
1621
  /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1622
+ color: theme.colors.secondary,
1656
1623
  dimColor: true,
1657
1624
  children: "directory:".padEnd(11)
1658
1625
  }), /* @__PURE__ */ jsx(Text, { children: directory })] })
@@ -1661,53 +1628,613 @@ function Header({ model, onLoad }) {
1661
1628
  });
1662
1629
  }
1663
1630
  //#endregion
1664
- //#region src/components/ModelPicker.tsx
1665
- function ModelPicker({ currentModel, onSelect, onClose }) {
1666
- const [options, setOptions] = useState([]);
1667
- const [error, setError] = useState(null);
1668
- const handleChange = useCallback((model) => {
1669
- onSelect({ model });
1670
- }, [onSelect]);
1671
- useInput(async (_input, key) => {
1672
- if (!error && options.length && key.return) {
1673
- await tick();
1674
- onClose();
1631
+ //#region src/components/ModelManager/ModelSuggestions.tsx
1632
+ function rankCatalogMatch(entry, normalizedInput) {
1633
+ const normalizedValue = entry.value.toLowerCase();
1634
+ const normalizedLabel = entry.label.toLowerCase();
1635
+ // v8 ignore start
1636
+ switch (true) {
1637
+ case normalizedValue.startsWith(normalizedInput): return 0;
1638
+ case normalizedLabel.startsWith(normalizedInput): return 1;
1639
+ case normalizedValue.includes(normalizedInput): return 2;
1640
+ case normalizedLabel.includes(normalizedInput): return 3;
1641
+ default: return Number.MAX_SAFE_INTEGER;
1642
+ }
1643
+ // v8 ignore stop
1644
+ }
1645
+ function ModelSuggestions({ catalog, input, isDisabled = false, onHighlight, onSelect }) {
1646
+ const normalizedInput = input.trim().toLowerCase();
1647
+ return /* @__PURE__ */ jsx(Suggestions, {
1648
+ isDisabled,
1649
+ options: useMemo(() => {
1650
+ if (!normalizedInput) return [];
1651
+ return catalog.filter((entry) => entry.value.toLowerCase().includes(normalizedInput) || entry.label.toLowerCase().includes(normalizedInput)).sort((left, right) => rankCatalogMatch(left, normalizedInput) - rankCatalogMatch(right, normalizedInput) || left.label.localeCompare(right.label)).map((entry) => ({
1652
+ label: entry.value,
1653
+ value: entry.value
1654
+ }));
1655
+ }, [catalog, normalizedInput]),
1656
+ resetKey: input,
1657
+ /* v8 ignore next */
1658
+ onHighlight: (option) => onHighlight?.(option?.value ?? null),
1659
+ onSelect: (option) => {
1660
+ onSelect(option.value);
1675
1661
  }
1676
1662
  });
1663
+ }
1664
+ //#endregion
1665
+ //#region src/components/ModelManager/types.ts
1666
+ var View = /* @__PURE__ */ function(View) {
1667
+ View["Menu"] = "menu";
1668
+ View["Switch"] = "switch";
1669
+ View["Download"] = "download";
1670
+ View["CustomDownload"] = "custom-download";
1671
+ View["Downloading"] = "downloading";
1672
+ View["Delete"] = "delete";
1673
+ View["DeleteConfirm"] = "delete-confirm";
1674
+ return View;
1675
+ }({});
1676
+ var MenuAction = /* @__PURE__ */ function(MenuAction) {
1677
+ MenuAction["Switch"] = "switch";
1678
+ MenuAction["Download"] = "download";
1679
+ MenuAction["Delete"] = "delete";
1680
+ MenuAction["Cancel"] = "cancel";
1681
+ return MenuAction;
1682
+ }({});
1683
+ var DownloadAction = /* @__PURE__ */ function(DownloadAction) {
1684
+ DownloadAction["Custom"] = "custom";
1685
+ return DownloadAction;
1686
+ }({});
1687
+ var ConfirmDeleteAction = /* @__PURE__ */ function(ConfirmDeleteAction) {
1688
+ ConfirmDeleteAction["Delete"] = "delete";
1689
+ return ConfirmDeleteAction;
1690
+ }({});
1691
+ //#endregion
1692
+ //#region src/components/ModelManager/utils.ts
1693
+ function buildMenuOptions() {
1694
+ return [
1695
+ {
1696
+ label: "Switch model",
1697
+ value: MenuAction.Switch
1698
+ },
1699
+ {
1700
+ label: "Download model",
1701
+ value: MenuAction.Download
1702
+ },
1703
+ {
1704
+ label: "Delete model",
1705
+ value: MenuAction.Delete
1706
+ },
1707
+ {
1708
+ label: "Cancel",
1709
+ value: MenuAction.Cancel
1710
+ }
1711
+ ];
1712
+ }
1713
+ function buildInstalledModelOptions(models, currentModel) {
1714
+ const nextModels = [...models];
1715
+ if (nextModels.includes(currentModel)) {
1716
+ nextModels.splice(nextModels.indexOf(currentModel), 1);
1717
+ nextModels.unshift(currentModel);
1718
+ }
1719
+ return nextModels.map((model) => ({
1720
+ label: model === currentModel ? `${model} (current model)` : model,
1721
+ value: model
1722
+ }));
1723
+ }
1724
+ function buildDownloadOptions(installedModels) {
1725
+ const installedModelSet = new Set(installedModels);
1726
+ const availableCatalog = CATALOG.filter(({ value, alias }) => !installedModelSet.has(value) && !(alias && installedModelSet.has(alias)));
1727
+ return [
1728
+ {
1729
+ label: "Enter custom model...",
1730
+ value: DownloadAction.Custom
1731
+ },
1732
+ ...availableCatalog.map(({ label, value }) => ({
1733
+ label,
1734
+ value
1735
+ })),
1736
+ BACK
1737
+ ];
1738
+ }
1739
+ function getNoticeColor(tone, theme) {
1740
+ switch (tone) {
1741
+ case "error": return theme.colors.error;
1742
+ case "success": return theme.colors.status;
1743
+ default: return theme.colors.secondary;
1744
+ }
1745
+ }
1746
+ function formatBytes(bytes) {
1747
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
1748
+ const units = [
1749
+ "B",
1750
+ "KB",
1751
+ "MB",
1752
+ "GB",
1753
+ "TB"
1754
+ ];
1755
+ let value = bytes;
1756
+ let index = 0;
1757
+ while (value >= 1024 && index < units.length - 1) {
1758
+ value /= 1024;
1759
+ index += 1;
1760
+ }
1761
+ const fractionDigits = value >= 10 || index === 0 ? 0 : 1;
1762
+ return `${value.toFixed(fractionDigits)} ${units[index]}`;
1763
+ }
1764
+ function isAbortError(error) {
1765
+ return error instanceof Error && error.name === "AbortError";
1766
+ }
1767
+ function mergeDownloadProgress(previous, model, status, completed, total) {
1768
+ const nextCompleted = typeof completed === "number" && Number.isFinite(completed) && completed >= 0 ? completed : null;
1769
+ const nextTotal = typeof total === "number" && Number.isFinite(total) && total > 0 ? total : null;
1770
+ if (nextTotal !== null && nextCompleted !== null) return {
1771
+ model,
1772
+ status,
1773
+ completed: nextCompleted,
1774
+ total: nextTotal
1775
+ };
1776
+ const hasPreviousProgress = previous?.model === model && previous.total > 0;
1777
+ return {
1778
+ model,
1779
+ status,
1780
+ completed: hasPreviousProgress ? previous.completed : 0,
1781
+ total: hasPreviousProgress ? previous.total : 0
1782
+ };
1783
+ }
1784
+ //#endregion
1785
+ //#region src/components/ModelManager/ModelCustomDownloadView.tsx
1786
+ function ModelCustomDownloadView({ downloadDraft, notice, theme, onDraftChange, onHighlight, onSelectSuggestion, onSubmit }) {
1787
+ const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
1788
+ color: getNoticeColor(notice.tone, theme),
1789
+ children: notice.text
1790
+ }) : null;
1791
+ return /* @__PURE__ */ jsxs(Box, {
1792
+ flexDirection: "column",
1793
+ children: [
1794
+ /* @__PURE__ */ jsx(Text, { children: "Enter an Ollama model name to download." }),
1795
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1796
+ value: downloadDraft,
1797
+ placeholder: "name:tag",
1798
+ wrapIndent: 2,
1799
+ onChange: onDraftChange,
1800
+ onSubmit
1801
+ })] }),
1802
+ /* @__PURE__ */ jsx(ModelSuggestions, {
1803
+ catalog: [],
1804
+ input: downloadDraft,
1805
+ onHighlight,
1806
+ onSelect: onSelectSuggestion
1807
+ }),
1808
+ renderNotice(),
1809
+ /* @__PURE__ */ jsx(Text, {
1810
+ color: theme.colors.secondary,
1811
+ dimColor: true,
1812
+ children: "Press Enter to download, Esc or Ctrl+C to go back."
1813
+ })
1814
+ ]
1815
+ });
1816
+ }
1817
+ //#endregion
1818
+ //#region src/components/ModelManager/ModelDeleteConfirmView.tsx
1819
+ function ModelDeleteConfirmView({ deleteCandidate, isDeleting, notice, theme, onCancel, onConfirm }) {
1820
+ if (isDeleting) return /* @__PURE__ */ jsx(Spinner, { label: `Deleting model ${deleteCandidate}...` });
1821
+ const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
1822
+ color: getNoticeColor(notice.tone, theme),
1823
+ children: notice.text
1824
+ }) : null;
1825
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1826
+ options: [{
1827
+ label: `Yes, delete ${deleteCandidate}`,
1828
+ value: ConfirmDeleteAction.Delete
1829
+ }, {
1830
+ ...BACK,
1831
+ label: "No"
1832
+ }],
1833
+ onCancel,
1834
+ onChange: onConfirm,
1835
+ children: [
1836
+ /* @__PURE__ */ jsxs(Text, { children: [
1837
+ WARNING,
1838
+ " Delete model",
1839
+ " ",
1840
+ /* @__PURE__ */ jsx(Text, {
1841
+ color: theme.colors.model,
1842
+ children: deleteCandidate
1843
+ }),
1844
+ "?"
1845
+ ] }),
1846
+ renderNotice(),
1847
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "This action cannot be undone" })
1848
+ ]
1849
+ });
1850
+ }
1851
+ //#endregion
1852
+ //#region src/components/ModelManager/ModelDeleteView.tsx
1853
+ function ModelDeleteView({ currentModel, installedModels, isLoading, notice, theme, onCancel, onSelect }) {
1854
+ if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1855
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1856
+ options: [...buildInstalledModelOptions(installedModels.filter((model) => model !== currentModel), currentModel), BACK],
1857
+ onCancel,
1858
+ onChange: (value) => {
1859
+ if (value === BACK.value) onCancel();
1860
+ else onSelect(value);
1861
+ },
1862
+ children: [
1863
+ /* @__PURE__ */ jsxs(Text, { children: [
1864
+ "Delete an installed model (current model",
1865
+ " ",
1866
+ /* @__PURE__ */ jsx(Text, {
1867
+ color: theme.colors.model,
1868
+ children: currentModel
1869
+ }),
1870
+ " cannot be deleted)."
1871
+ ] }),
1872
+ notice && /* @__PURE__ */ jsx(Text, {
1873
+ color: getNoticeColor(notice.tone, theme),
1874
+ children: notice.text
1875
+ }),
1876
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Delete models" })
1877
+ ]
1878
+ });
1879
+ }
1880
+ //#endregion
1881
+ //#region src/components/ModelManager/ModelDownloadingView.tsx
1882
+ function ModelDownloadingView({ progress, theme, onCancel }) {
1883
+ const percent = progress.total > 0 && Number.isFinite(progress.completed) && Number.isFinite(progress.total) ? Math.round(progress.completed / progress.total * 100) : null;
1884
+ return /* @__PURE__ */ jsxs(Box, {
1885
+ flexDirection: "column",
1886
+ children: [
1887
+ /* @__PURE__ */ jsxs(Text, { children: [
1888
+ "Downloading model:",
1889
+ " ",
1890
+ /* @__PURE__ */ jsx(Text, {
1891
+ color: theme.colors.model,
1892
+ children: progress.model
1893
+ })
1894
+ ] }),
1895
+ /* @__PURE__ */ jsx(Text, { children: progress.status }),
1896
+ percent !== null ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, { children: [
1897
+ percent,
1898
+ "% (",
1899
+ formatBytes(progress.completed),
1900
+ " /",
1901
+ " ",
1902
+ formatBytes(progress.total),
1903
+ ")"
1904
+ ] }), /* @__PURE__ */ jsx(ProgressBar, { value: Math.max(0, Math.min(100, percent)) })] }) : /* @__PURE__ */ jsx(Text, {
1905
+ color: theme.colors.secondary,
1906
+ dimColor: true,
1907
+ children: "Progress details unavailable. Waiting for Ollama updates..."
1908
+ }),
1909
+ /* @__PURE__ */ jsx(SelectPrompt, {
1910
+ options: [{
1911
+ label: "Cancel download",
1912
+ value: "cancel-download"
1913
+ }],
1914
+ onCancel,
1915
+ onChange: onCancel,
1916
+ children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Press Enter, Esc, or Ctrl+C to cancel" })
1917
+ })
1918
+ ]
1919
+ });
1920
+ }
1921
+ //#endregion
1922
+ //#region src/components/ModelManager/ModelDownloadView.tsx
1923
+ function ModelDownloadView({ installedModels, notice, theme, onCancel, onChange }) {
1924
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1925
+ options: buildDownloadOptions(installedModels),
1926
+ onCancel,
1927
+ onChange: (value) => {
1928
+ switch (value) {
1929
+ case "custom":
1930
+ onChange("custom");
1931
+ break;
1932
+ case "back":
1933
+ onCancel();
1934
+ break;
1935
+ default:
1936
+ onChange(value);
1937
+ break;
1938
+ }
1939
+ },
1940
+ children: [
1941
+ /* @__PURE__ */ jsx(Text, { children: "Choose a model to download or use a custom model name." }),
1942
+ notice && /* @__PURE__ */ jsx(Text, {
1943
+ color: getNoticeColor(notice.tone, theme),
1944
+ children: notice.text
1945
+ }),
1946
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Download models" })
1947
+ ]
1948
+ });
1949
+ }
1950
+ //#endregion
1951
+ //#region src/components/ModelManager/ModelSwitchView.tsx
1952
+ function ModelSwitchView({ currentModel, installedModels, isLoading, onCancel, onSelect }) {
1953
+ if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1954
+ return /* @__PURE__ */ jsx(SelectPrompt, {
1955
+ options: [...buildInstalledModelOptions(installedModels, currentModel), BACK],
1956
+ onCancel,
1957
+ onChange: (value) => {
1958
+ if (value === BACK.value) onCancel();
1959
+ else onSelect(value);
1960
+ },
1961
+ children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Switch models" })
1962
+ });
1963
+ }
1964
+ //#endregion
1965
+ //#region src/components/ModelManager/ModelManager.tsx
1966
+ function ModelManager({ currentModel, onSelect, onClose, theme = getTheme() }) {
1967
+ const [view, setView] = useState(View.Menu);
1968
+ const [installedModels, setInstalledModels] = useState([]);
1969
+ const [isLoadingModels, setIsLoadingModels] = useState(true);
1970
+ const [loadError, setLoadError] = useState(null);
1971
+ const [notice, setNotice] = useState(null);
1972
+ const [downloadDraft, setDownloadDraft] = useState("");
1973
+ const [highlightedSuggestion, setHighlightedSuggestion] = useState(null);
1974
+ const [downloadProgress, setDownloadProgress] = useState(null);
1975
+ const [deleteCandidate, setDeleteCandidate] = useState(null);
1976
+ const [isDeleting, setIsDeleting] = useState(false);
1977
+ const isDeletingRef = useRef(false);
1978
+ const pullRef = useRef(null);
1979
+ const loadInstalledModels = useCallback(async () => {
1980
+ setIsLoadingModels(true);
1981
+ setLoadError(null);
1982
+ try {
1983
+ setInstalledModels(await listModels());
1984
+ } catch (error) {
1985
+ setLoadError(error instanceof Error ? error.message : /* v8 ignore next */ String(error));
1986
+ } finally {
1987
+ setIsLoadingModels(false);
1988
+ }
1989
+ }, []);
1677
1990
  useEffect(() => {
1678
- async function load() {
1679
- try {
1680
- const models = await listModels();
1681
- if (models.includes(currentModel)) {
1682
- models.splice(models.indexOf(currentModel), 1);
1683
- models.unshift(currentModel);
1684
- }
1685
- setOptions(models.map((model) => ({
1686
- label: model,
1687
- value: model
1688
- })));
1689
- } catch (error) {
1690
- setError(error instanceof Error ? error.message : String(error));
1991
+ loadInstalledModels();
1992
+ }, [loadInstalledModels]);
1993
+ const resetDownloadState = useCallback(() => {
1994
+ setDownloadDraft("");
1995
+ setHighlightedSuggestion(null);
1996
+ setDownloadProgress(null);
1997
+ pullRef.current = null;
1998
+ }, []);
1999
+ const handleBackToMenu = useCallback(() => {
2000
+ setNotice(null);
2001
+ setDeleteCandidate(null);
2002
+ setIsDeleting(false);
2003
+ isDeletingRef.current = false;
2004
+ resetDownloadState();
2005
+ setView(View.Menu);
2006
+ }, [resetDownloadState]);
2007
+ const cancelActivePull = useCallback(() => {
2008
+ pullRef.current?.abort();
2009
+ }, []);
2010
+ useInput((input, key) => {
2011
+ if (view === View.CustomDownload && (key.escape || key.ctrl && input === "c")) {
2012
+ setNotice(null);
2013
+ setHighlightedSuggestion(null);
2014
+ setView(View.Download);
2015
+ return;
2016
+ }
2017
+ if (view === View.Downloading && (key.escape || key.ctrl && input === "c")) cancelActivePull();
2018
+ });
2019
+ const handleMenuChange = useCallback((value) => {
2020
+ setNotice(null);
2021
+ switch (value) {
2022
+ case "switch":
2023
+ setView(View.Switch);
2024
+ break;
2025
+ case "download":
2026
+ setView(View.Download);
2027
+ break;
2028
+ case "delete":
2029
+ setView(View.Delete);
2030
+ break;
2031
+ default: onClose();
2032
+ }
2033
+ }, [onClose]);
2034
+ const handleSwitchChange = useCallback((model) => {
2035
+ onSelect({ model });
2036
+ }, [onSelect]);
2037
+ const startPull = useCallback(async (model) => {
2038
+ const normalizedModel = model.trim();
2039
+ if (!normalizedModel) {
2040
+ setNotice({
2041
+ tone: "error",
2042
+ text: `❗ Enter a model name to download`
2043
+ });
2044
+ return;
2045
+ }
2046
+ if (installedModels.includes(normalizedModel)) {
2047
+ setNotice({
2048
+ tone: "info",
2049
+ text: `${normalizedModel} is already installed`
2050
+ });
2051
+ return;
2052
+ }
2053
+ setNotice(null);
2054
+ setDownloadProgress({
2055
+ model: normalizedModel,
2056
+ status: "Starting download...",
2057
+ completed: 0,
2058
+ total: 0
2059
+ });
2060
+ setView(View.Downloading);
2061
+ try {
2062
+ const pull = await pullModel(normalizedModel);
2063
+ pullRef.current = pull;
2064
+ for await (const update of pull) setDownloadProgress((previous) => {
2065
+ return mergeDownloadProgress(previous, normalizedModel, update.status, update.completed, update.total);
2066
+ });
2067
+ pullRef.current = null;
2068
+ resetDownloadState();
2069
+ await loadInstalledModels();
2070
+ setNotice({
2071
+ tone: "success",
2072
+ text: `✅ ${normalizedModel} downloaded successfully`
2073
+ });
2074
+ setView(View.Menu);
2075
+ } catch (error) {
2076
+ pullRef.current = null;
2077
+ if (isAbortError(error)) {
2078
+ setNotice({
2079
+ tone: "error",
2080
+ text: `❌ Download canceled for ${normalizedModel}`
2081
+ });
2082
+ setDownloadProgress(null);
2083
+ setView(View.Download);
2084
+ return;
1691
2085
  }
2086
+ setNotice({
2087
+ tone: "error",
2088
+ text: `❗ Error downloading model: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`
2089
+ });
2090
+ setDownloadProgress(null);
2091
+ setView(View.CustomDownload);
2092
+ }
2093
+ }, [
2094
+ installedModels,
2095
+ loadInstalledModels,
2096
+ resetDownloadState
2097
+ ]);
2098
+ const handleDownloadChange = useCallback((value) => {
2099
+ if (value === "custom") {
2100
+ setNotice(null);
2101
+ setView(View.CustomDownload);
2102
+ return;
2103
+ }
2104
+ // v8 ignore next 3
2105
+ if (value === "back") {
2106
+ handleBackToMenu();
2107
+ return;
2108
+ }
2109
+ setDownloadDraft(value);
2110
+ startPull(value);
2111
+ }, [handleBackToMenu, startPull]);
2112
+ const handleCustomDownloadSubmit = useCallback((value) => {
2113
+ const nextValue = highlightedSuggestion ?? value.trim();
2114
+ setDownloadDraft(nextValue);
2115
+ startPull(nextValue);
2116
+ }, [highlightedSuggestion, startPull]);
2117
+ const handleDeleteChange = useCallback((model) => {
2118
+ setNotice(null);
2119
+ setDeleteCandidate(model);
2120
+ setView(View.DeleteConfirm);
2121
+ }, []);
2122
+ const handleDeleteConfirm = useCallback(async (value) => {
2123
+ if (isDeletingRef.current) return;
2124
+ if (value === "back") {
2125
+ setView(View.Delete);
2126
+ return;
2127
+ }
2128
+ // v8 ignore next 3
2129
+ if (!deleteCandidate) {
2130
+ setView(View.Delete);
2131
+ return;
2132
+ }
2133
+ try {
2134
+ isDeletingRef.current = true;
2135
+ setIsDeleting(true);
2136
+ await deleteModel(deleteCandidate);
2137
+ await loadInstalledModels();
2138
+ setNotice({
2139
+ tone: "success",
2140
+ text: `✅ ${deleteCandidate} deleted successfully`
2141
+ });
2142
+ isDeletingRef.current = false;
2143
+ setIsDeleting(false);
2144
+ setDeleteCandidate(null);
2145
+ setView(View.Delete);
2146
+ } catch (error) {
2147
+ isDeletingRef.current = false;
2148
+ setIsDeleting(false);
2149
+ setNotice({
2150
+ tone: "error",
2151
+ text: `❗ Error deleting model: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`
2152
+ });
2153
+ setView(View.Delete);
1692
2154
  }
1693
- load();
1694
- }, [currentModel]);
1695
- if (error) return /* @__PURE__ */ jsxs(Text, {
1696
- color: "red",
1697
- children: ["Error loading models: ", error]
2155
+ }, [deleteCandidate, loadInstalledModels]);
2156
+ const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
2157
+ color: notice.tone === "error" ? theme.colors.error : notice.tone === "success" ? theme.colors.status : theme.colors.secondary,
2158
+ children: notice.text
2159
+ }) : null;
2160
+ if (loadError && view !== View.Menu) return /* @__PURE__ */ jsxs(Box, {
2161
+ flexDirection: "column",
2162
+ children: [/* @__PURE__ */ jsxs(Text, {
2163
+ color: theme.colors.error,
2164
+ children: ["Error loading models: ", loadError]
2165
+ }), /* @__PURE__ */ jsx(Text, {
2166
+ color: theme.colors.secondary,
2167
+ dimColor: true,
2168
+ children: "Press Esc to go back."
2169
+ })]
1698
2170
  });
1699
- if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1700
- return /* @__PURE__ */ jsx(SelectPrompt, {
1701
- options,
1702
- defaultValue: currentModel,
1703
- onChange: handleChange,
2171
+ if (view === View.Downloading && downloadProgress) return /* @__PURE__ */ jsx(ModelDownloadingView, {
2172
+ progress: downloadProgress,
2173
+ theme,
2174
+ onCancel: cancelActivePull
2175
+ });
2176
+ if (view === View.CustomDownload) return /* @__PURE__ */ jsx(ModelCustomDownloadView, {
2177
+ downloadDraft,
2178
+ notice,
2179
+ theme,
2180
+ onDraftChange: setDownloadDraft,
2181
+ onHighlight: setHighlightedSuggestion,
2182
+ onSelectSuggestion: (value) => {
2183
+ setDownloadDraft(value);
2184
+ setHighlightedSuggestion(value);
2185
+ },
2186
+ onSubmit: handleCustomDownloadSubmit
2187
+ });
2188
+ if (view === View.Switch) return /* @__PURE__ */ jsx(ModelSwitchView, {
2189
+ currentModel,
2190
+ installedModels,
2191
+ isLoading: isLoadingModels,
2192
+ onCancel: handleBackToMenu,
2193
+ onSelect: handleSwitchChange
2194
+ });
2195
+ if (view === View.Download) return /* @__PURE__ */ jsx(ModelDownloadView, {
2196
+ installedModels,
2197
+ notice,
2198
+ theme,
2199
+ onCancel: handleBackToMenu,
2200
+ onChange: handleDownloadChange
2201
+ });
2202
+ if (view === View.Delete) return /* @__PURE__ */ jsx(ModelDeleteView, {
2203
+ currentModel,
2204
+ installedModels,
2205
+ isLoading: isLoadingModels,
2206
+ notice,
2207
+ theme,
2208
+ onCancel: handleBackToMenu,
2209
+ onSelect: handleDeleteChange
2210
+ });
2211
+ if (view === View.DeleteConfirm && deleteCandidate) return /* @__PURE__ */ jsx(ModelDeleteConfirmView, {
2212
+ deleteCandidate,
2213
+ isDeleting,
2214
+ notice,
2215
+ theme,
2216
+ onCancel: () => {
2217
+ setView(View.Delete);
2218
+ },
2219
+ onConfirm: handleDeleteConfirm
2220
+ });
2221
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
2222
+ options: buildMenuOptions(),
1704
2223
  onCancel: onClose,
1705
- children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
2224
+ onChange: handleMenuChange,
2225
+ children: [
2226
+ /* @__PURE__ */ jsxs(Text, { children: ["Current model: ", /* @__PURE__ */ jsx(Text, {
2227
+ color: theme.colors.model,
2228
+ children: currentModel
2229
+ })] }),
2230
+ renderNotice(),
2231
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage models" })
2232
+ ]
1706
2233
  });
1707
2234
  }
1708
2235
  //#endregion
1709
2236
  //#region src/components/SearchSettings.tsx
1710
- function SearchSettings({ currentUrl, onClose, onSave }) {
2237
+ function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
1711
2238
  const [view, setView] = useState("menu");
1712
2239
  const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1713
2240
  const [error, setError] = useState(null);
@@ -1779,10 +2306,11 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1779
2306
  placeholder: "http://localhost:8080"
1780
2307
  })] }),
1781
2308
  error && /* @__PURE__ */ jsx(Text, {
1782
- color: "red",
2309
+ color: theme.colors.error,
1783
2310
  children: error
1784
2311
  }),
1785
2312
  /* @__PURE__ */ jsx(Text, {
2313
+ color: theme.colors.secondary,
1786
2314
  dimColor: true,
1787
2315
  children: "Press Enter to save, Esc to go back."
1788
2316
  })
@@ -1793,10 +2321,14 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1793
2321
  onChange: handleChange,
1794
2322
  onCancel: onClose,
1795
2323
  children: [
1796
- /* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
1797
- color: "cyan",
1798
- children: currentUrl ?? "not set"
1799
- })] }),
2324
+ /* @__PURE__ */ jsxs(Text, { children: [
2325
+ "SearXNG URL:",
2326
+ " ",
2327
+ /* @__PURE__ */ jsx(Text, {
2328
+ color: theme.colors.status,
2329
+ children: currentUrl ?? "not set"
2330
+ })
2331
+ ] }),
1800
2332
  /* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
1801
2333
  /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
1802
2334
  ]
@@ -1805,7 +2337,6 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1805
2337
  //#endregion
1806
2338
  //#region src/components/SessionManager.tsx
1807
2339
  var ACTION = {
1808
- BACK: "back",
1809
2340
  CLOSE: "close",
1810
2341
  DELETE_MENU: "delete-menu",
1811
2342
  DELETE_PREFIX: "delete:",
@@ -1823,7 +2354,7 @@ function formatSessionLabel(session, maxWidth, prefix = "") {
1823
2354
  if (availableTitleWidth < 1) return truncate(`${prefix}${session.title}${suffix}`, maxWidth);
1824
2355
  return `${prefix}${truncate(session.title, availableTitleWidth)}${suffix}`;
1825
2356
  }
1826
- function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
2357
+ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, theme = getTheme() }) {
1827
2358
  const [view, setView] = useState("main");
1828
2359
  const [error, setError] = useState();
1829
2360
  const [, refreshSessionList] = useState(0);
@@ -1833,16 +2364,10 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1833
2364
  const options = view === "open" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1834
2365
  label: formatSessionLabel(session, maxLabelWidth),
1835
2366
  value: `${ACTION.OPEN_PREFIX}${session.id}`
1836
- })), {
1837
- label: "Back",
1838
- value: ACTION.BACK
1839
- }] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2367
+ })), BACK] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1840
2368
  label: formatSessionLabel(session, maxLabelWidth, "Delete "),
1841
2369
  value: `${ACTION.DELETE_PREFIX}${session.id}`
1842
- })), {
1843
- label: "Back",
1844
- value: ACTION.BACK
1845
- }] : [
2370
+ })), BACK] : [
1846
2371
  {
1847
2372
  label: "New session",
1848
2373
  value: ACTION.NEW
@@ -1874,7 +2399,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1874
2399
  case value === ACTION.OPEN_MENU:
1875
2400
  setView("open");
1876
2401
  break;
1877
- case value === ACTION.BACK:
2402
+ case value === BACK.value:
1878
2403
  setView("main");
1879
2404
  break;
1880
2405
  case value.startsWith(ACTION.DELETE_PREFIX):
@@ -1909,7 +2434,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1909
2434
  error && /* @__PURE__ */ jsx(Box, {
1910
2435
  marginBottom: 1,
1911
2436
  children: /* @__PURE__ */ jsx(Text, {
1912
- color: "red",
2437
+ color: theme.colors.error,
1913
2438
  children: error
1914
2439
  })
1915
2440
  }),
@@ -1922,25 +2447,177 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1922
2447
  });
1923
2448
  }
1924
2449
  //#endregion
1925
- //#region src/components/App.tsx
1926
- function createSession(sessionId, model) {
1927
- return sessionId ? loadSession(sessionId) : createSession$1(model);
2450
+ //#region src/components/ThemeSettings.tsx
2451
+ function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
2452
+ const [selectedIndex, setSelectedIndex] = useState(() => {
2453
+ const initialIndex = LIST$1.findIndex(({ id }) => id === currentTheme);
2454
+ return initialIndex >= 0 ? initialIndex : 0;
2455
+ });
2456
+ const selectedTheme = useMemo(
2457
+ // v8 ignore next
2458
+ () => LIST$1[selectedIndex] ?? getTheme(),
2459
+ [selectedIndex]
2460
+ );
2461
+ useEffect(() => {
2462
+ onPreview(selectedTheme.id);
2463
+ }, [onPreview, selectedTheme.id]);
2464
+ useInput((input, key) => {
2465
+ if (key.escape || key.ctrl && input === "c") {
2466
+ onClose();
2467
+ return;
2468
+ }
2469
+ if (key.upArrow) {
2470
+ setSelectedIndex((current) => current === 0 ? LIST$1.length - 1 : current - 1);
2471
+ return;
2472
+ }
2473
+ if (key.downArrow) {
2474
+ setSelectedIndex((current) => current === LIST$1.length - 1 ? 0 : current + 1);
2475
+ return;
2476
+ }
2477
+ if (key.return) onSave(selectedTheme.id);
2478
+ });
2479
+ return /* @__PURE__ */ jsxs(Box, {
2480
+ flexDirection: "column",
2481
+ children: [
2482
+ /* @__PURE__ */ jsxs(Text, { children: [
2483
+ "Theme:",
2484
+ " ",
2485
+ /* @__PURE__ */ jsx(Text, {
2486
+ color: selectedTheme.colors.accent,
2487
+ children: selectedTheme.label
2488
+ })
2489
+ ] }),
2490
+ /* @__PURE__ */ jsx(Text, {
2491
+ color: selectedTheme.colors.secondary,
2492
+ children: selectedTheme.description
2493
+ }),
2494
+ /* @__PURE__ */ jsx(Box, {
2495
+ flexDirection: "column",
2496
+ marginTop: 1,
2497
+ children: LIST$1.map((theme, index) => {
2498
+ const isSelected = index === selectedIndex;
2499
+ return /* @__PURE__ */ jsxs(Text, {
2500
+ color: isSelected ? selectedTheme.colors.accent : void 0,
2501
+ children: [
2502
+ isSelected ? "›" : " ",
2503
+ " ",
2504
+ theme.label
2505
+ ]
2506
+ }, theme.id);
2507
+ })
2508
+ }),
2509
+ /* @__PURE__ */ jsxs(Box, {
2510
+ borderColor: selectedTheme.colors.border,
2511
+ borderStyle: "round",
2512
+ flexDirection: "column",
2513
+ marginTop: 1,
2514
+ paddingX: 1,
2515
+ children: [
2516
+ /* @__PURE__ */ jsxs(Text, {
2517
+ color: selectedTheme.colors.status,
2518
+ children: [HEADER_PREFIX, " Preview"]
2519
+ }),
2520
+ /* @__PURE__ */ jsx(Text, {
2521
+ color: selectedTheme.colors.secondary,
2522
+ children: "Markdown and code styling follow the selected theme."
2523
+ }),
2524
+ /* @__PURE__ */ jsxs(Text, { children: [
2525
+ "Status accent:",
2526
+ " ",
2527
+ /* @__PURE__ */ jsx(Text, {
2528
+ color: selectedTheme.colors.status,
2529
+ children: "search enabled"
2530
+ })
2531
+ ] }),
2532
+ /* @__PURE__ */ jsx(CodeBlock, {
2533
+ code: "const theme = 'preview';",
2534
+ language: "ts",
2535
+ role: "assistant",
2536
+ theme: selectedTheme
2537
+ })
2538
+ ]
2539
+ }),
2540
+ /* @__PURE__ */ jsx(Box, {
2541
+ marginTop: 1,
2542
+ children: /* @__PURE__ */ jsx(SelectPromptHint, {
2543
+ message: "Preview theme",
2544
+ escapeLabel: "cancel and restore"
2545
+ })
2546
+ })
2547
+ ]
2548
+ });
1928
2549
  }
1929
- function App({ sessionId }) {
2550
+ //#endregion
2551
+ //#region src/components/App/constants.ts
2552
+ var SCREEN = /* @__PURE__ */ function(SCREEN) {
2553
+ SCREEN["CHAT"] = "chat";
2554
+ SCREEN["MODEL_MANAGER"] = "model-manager";
2555
+ SCREEN["SEARCH_SETTINGS"] = "search-settings";
2556
+ SCREEN["SESSION_MANAGER"] = "session-manager";
2557
+ SCREEN["THEME_SETTINGS"] = "theme-settings";
2558
+ return SCREEN;
2559
+ }({});
2560
+ //#endregion
2561
+ //#region src/components/App/hooks/useScreenRouter.ts
2562
+ function useScreenRouter() {
1930
2563
  const { exit } = useApp();
1931
- const [appConfig, setConfig] = useState(() => loadConfig());
1932
- const [currentScreen, setScreen] = useState("chat");
1933
- const [mode, setMode] = useState(SAFE);
1934
- const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
1935
- const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
2564
+ const [currentScreen, setScreen] = useState(SCREEN.CHAT);
2565
+ return {
2566
+ currentScreen,
2567
+ setScreen,
2568
+ handleClose: useCallback(() => {
2569
+ setScreen(SCREEN.CHAT);
2570
+ }, []),
2571
+ handleCommand: useCallback((command, callbacks) => {
2572
+ const { onCreateSession, onSetPreviewThemeId, model, theme } = callbacks;
2573
+ switch (command) {
2574
+ case "/session":
2575
+ setScreen(SCREEN.SESSION_MANAGER);
2576
+ break;
2577
+ case "/model":
2578
+ setScreen(SCREEN.MODEL_MANAGER);
2579
+ break;
2580
+ case "/search":
2581
+ setScreen(SCREEN.SEARCH_SETTINGS);
2582
+ break;
2583
+ case "/theme":
2584
+ onSetPreviewThemeId(theme);
2585
+ setScreen(SCREEN.THEME_SETTINGS);
2586
+ break;
2587
+ case "/clear": {
2588
+ resetSystemMessage();
2589
+ const nextSession = onCreateSession(model);
2590
+ setScreen(SCREEN.CHAT);
2591
+ clear(nextSession.metadata.id);
2592
+ break;
2593
+ }
2594
+ case "/exit":
2595
+ exit();
2596
+ break;
2597
+ }
2598
+ }, [exit])
2599
+ };
2600
+ }
2601
+ //#endregion
2602
+ //#region src/components/App/hooks/useSessionManager.ts
2603
+ function useSessionManager({ sessionId, model, commandColor }) {
2604
+ const [activeSession, setSession] = useState(() => sessionId ? loadSession(sessionId) : createSession(model));
1936
2605
  const sessionRef = useRef(activeSession);
2606
+ const commandColorRef = useRef(commandColor);
2607
+ const modelRef = useRef(model);
1937
2608
  useEffect(() => {
1938
2609
  sessionRef.current = activeSession;
1939
2610
  }, [activeSession]);
2611
+ useEffect(() => {
2612
+ commandColorRef.current = commandColor;
2613
+ }, [commandColor]);
2614
+ useEffect(() => {
2615
+ modelRef.current = model;
2616
+ }, [model]);
1940
2617
  useEffect(() => {
1941
2618
  return () => {
1942
2619
  const currentSession = sessionRef.current;
1943
- if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, "cyan")}\n`);
2620
+ if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, commandColorRef.current)}\n`);
1944
2621
  };
1945
2622
  }, []);
1946
2623
  const setActiveSession = useCallback((nextSession) => {
@@ -1949,73 +2626,76 @@ function App({ sessionId }) {
1949
2626
  return nextSession;
1950
2627
  });
1951
2628
  }, []);
1952
- const handleHeaderLoad = useCallback(() => {
1953
- setIsHeaderLoaded(true);
2629
+ return {
2630
+ activeSession,
2631
+ setSession,
2632
+ handleCreateSession: useCallback(() => {
2633
+ const nextSession = createSession(modelRef.current);
2634
+ setActiveSession(nextSession);
2635
+ clear(nextSession.metadata.id);
2636
+ return nextSession;
2637
+ }, [setActiveSession]),
2638
+ handleOpenSession: useCallback((sessionId) => {
2639
+ if (sessionRef.current.metadata.id === sessionId) return false;
2640
+ setActiveSession(loadSession(sessionId));
2641
+ clear(sessionId);
2642
+ return true;
2643
+ }, [setActiveSession]),
2644
+ handleDeleteSession: useCallback((sessionId) => {
2645
+ deleteSession(sessionId);
2646
+ setSession((current) => {
2647
+ if (current.metadata.id !== sessionId) return current;
2648
+ return createSession(modelRef.current);
2649
+ });
2650
+ }, []),
2651
+ handleMessagesChange: useCallback((messages) => {
2652
+ setSession((current) => {
2653
+ const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
2654
+ if (persistedMessages.length <= current.messages.length) return current;
2655
+ let metadata = current.metadata;
2656
+ for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, modelRef.current);
2657
+ return {
2658
+ metadata,
2659
+ messages: persistedMessages
2660
+ };
2661
+ });
2662
+ }, [])
2663
+ };
2664
+ }
2665
+ //#endregion
2666
+ //#region src/components/App/hooks/useThemeSettings.ts
2667
+ function useThemeSettings({ currentTheme, onUpdateConfig, setScreen }) {
2668
+ const [previewThemeId, setPreviewThemeId] = useState(null);
2669
+ const activeTheme = getTheme(previewThemeId ?? currentTheme);
2670
+ const handleThemePreview = useCallback((themeId) => {
2671
+ setPreviewThemeId(themeId);
1954
2672
  }, []);
1955
- const handleCreateSession = useCallback(() => {
1956
- const nextSession = createSession$1(appConfig.model);
1957
- setActiveSession(nextSession);
1958
- setScreen("chat");
1959
- clear(nextSession.metadata.id);
1960
- return nextSession;
1961
- }, [appConfig.model, setActiveSession]);
1962
- const handleOpenSession = useCallback((sessionId) => {
1963
- if (sessionRef.current.metadata.id === sessionId) {
1964
- setScreen("chat");
1965
- return;
1966
- }
1967
- setActiveSession(loadSession(sessionId));
1968
- setScreen("chat");
1969
- clear(sessionId);
1970
- }, [setActiveSession]);
1971
- const handleDeleteSession = useCallback((sessionId) => {
1972
- deleteSession(sessionId);
1973
- setSession((current) => {
1974
- if (current.metadata.id !== sessionId) return current;
1975
- return createSession$1(appConfig.model);
1976
- });
1977
- setScreen("session-manager");
1978
- }, [appConfig.model]);
1979
- const handleMessagesChange = useCallback((messages) => {
1980
- setSession((current) => {
1981
- const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
1982
- if (persistedMessages.length <= current.messages.length) return current;
1983
- let metadata = current.metadata;
1984
- for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, appConfig.model);
1985
- return {
1986
- metadata,
1987
- messages: persistedMessages
1988
- };
1989
- });
1990
- }, [appConfig.model]);
1991
- const handleCommand = useCallback((command) => {
1992
- switch (command) {
1993
- case "/session":
1994
- setScreen("session-manager");
1995
- break;
1996
- case "/model":
1997
- setScreen("model-picker");
1998
- break;
1999
- case "/search":
2000
- setScreen("search-settings");
2001
- break;
2002
- case "/clear": {
2003
- resetSystemMessage();
2004
- setScreen("chat");
2005
- const nextSession = createSession$1(appConfig.model);
2006
- setActiveSession(nextSession);
2007
- clear(nextSession.metadata.id);
2008
- break;
2009
- }
2010
- case "/exit":
2011
- exit();
2012
- break;
2013
- }
2014
- }, [
2015
- appConfig.model,
2016
- exit,
2017
- setActiveSession
2018
- ]);
2673
+ return {
2674
+ activeTheme,
2675
+ handleThemeClose: useCallback(() => {
2676
+ setPreviewThemeId(null);
2677
+ setScreen(SCREEN.CHAT);
2678
+ }, [setScreen]),
2679
+ handleThemePreview,
2680
+ handleThemeSave: useCallback((themeId) => {
2681
+ setPreviewThemeId(null);
2682
+ onUpdateConfig({ theme: themeId });
2683
+ }, [onUpdateConfig]),
2684
+ setPreviewThemeId
2685
+ };
2686
+ }
2687
+ //#endregion
2688
+ //#region src/components/App/App.tsx
2689
+ function App({ sessionId }) {
2690
+ const [appConfig, setConfig] = useState(() => loadConfig());
2691
+ const [mode, setMode] = useState(SAFE);
2692
+ const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
2693
+ const { currentScreen, setScreen, handleClose, handleCommand } = useScreenRouter();
2694
+ const { activeSession, setSession, handleCreateSession, handleOpenSession, handleDeleteSession, handleMessagesChange } = useSessionManager({
2695
+ sessionId,
2696
+ model: appConfig.model,
2697
+ commandColor: getTheme(appConfig.theme).colors.command
2698
+ });
2019
2699
  const handleUpdateConfig = useCallback((update) => {
2020
2700
  setConfig((current) => ({
2021
2701
  ...current,
@@ -2027,10 +2707,15 @@ function App({ sessionId }) {
2027
2707
  ...current,
2028
2708
  metadata: updateSessionModel(current.metadata.id, newModel)
2029
2709
  }));
2030
- setScreen("chat");
2031
- }, []);
2032
- const handleClose = useCallback(() => {
2033
- setScreen("chat");
2710
+ setScreen(SCREEN.CHAT);
2711
+ }, [setScreen, setSession]);
2712
+ const { activeTheme, handleThemeClose, handleThemePreview, handleThemeSave, setPreviewThemeId } = useThemeSettings({
2713
+ currentTheme: appConfig.theme,
2714
+ onUpdateConfig: handleUpdateConfig,
2715
+ setScreen
2716
+ });
2717
+ const handleHeaderLoad = useCallback(() => {
2718
+ setIsHeaderLoaded(true);
2034
2719
  }, []);
2035
2720
  const handleToggleMode = useCallback(() => {
2036
2721
  setMode((mode) => {
@@ -2042,40 +2727,78 @@ function App({ sessionId }) {
2042
2727
  }
2043
2728
  });
2044
2729
  }, []);
2730
+ const handleChatCommand = useCallback((command) => {
2731
+ handleCommand(command, {
2732
+ model: appConfig.model,
2733
+ theme: appConfig.theme,
2734
+ onCreateSession: handleCreateSession,
2735
+ onSetPreviewThemeId: setPreviewThemeId
2736
+ });
2737
+ }, [
2738
+ appConfig.model,
2739
+ appConfig.theme,
2740
+ handleCommand,
2741
+ handleCreateSession,
2742
+ setPreviewThemeId
2743
+ ]);
2744
+ const handleDeleteSessionAndStay = useCallback((sid) => {
2745
+ handleDeleteSession(sid);
2746
+ setScreen(SCREEN.SESSION_MANAGER);
2747
+ }, [handleDeleteSession, setScreen]);
2748
+ const handleOpenSessionAndNavigate = useCallback((sid) => {
2749
+ handleOpenSession(sid);
2750
+ setScreen(SCREEN.CHAT);
2751
+ }, [handleOpenSession, setScreen]);
2752
+ const handleCreateSessionAndNavigate = useCallback(() => {
2753
+ handleCreateSession();
2754
+ setScreen(SCREEN.CHAT);
2755
+ }, [handleCreateSession, setScreen]);
2045
2756
  let screenContent;
2046
2757
  switch (currentScreen) {
2047
- case "model-picker":
2048
- screenContent = /* @__PURE__ */ jsx(ModelPicker, {
2758
+ case SCREEN.MODEL_MANAGER:
2759
+ screenContent = /* @__PURE__ */ jsx(ModelManager, {
2049
2760
  currentModel: appConfig.model,
2050
2761
  onSelect: handleUpdateConfig,
2051
- onClose: handleClose
2762
+ onClose: handleClose,
2763
+ theme: activeTheme
2052
2764
  });
2053
2765
  break;
2054
- case "search-settings":
2766
+ case SCREEN.SEARCH_SETTINGS:
2055
2767
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
2056
2768
  currentUrl: appConfig.searxngBaseUrl,
2057
2769
  onSave: handleUpdateConfig,
2058
- onClose: handleClose
2770
+ onClose: handleClose,
2771
+ theme: activeTheme
2059
2772
  });
2060
2773
  break;
2061
- case "session-manager":
2774
+ case SCREEN.SESSION_MANAGER:
2062
2775
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
2063
2776
  currentSessionId: activeSession.metadata.id,
2064
2777
  onClose: handleClose,
2065
- onDelete: handleDeleteSession,
2066
- onNew: handleCreateSession,
2067
- onOpen: handleOpenSession
2778
+ onDelete: handleDeleteSessionAndStay,
2779
+ onNew: handleCreateSessionAndNavigate,
2780
+ onOpen: handleOpenSessionAndNavigate,
2781
+ theme: activeTheme
2782
+ });
2783
+ break;
2784
+ case SCREEN.THEME_SETTINGS:
2785
+ screenContent = /* @__PURE__ */ jsx(ThemeSettings, {
2786
+ currentTheme: appConfig.theme,
2787
+ onClose: handleThemeClose,
2788
+ onPreview: handleThemePreview,
2789
+ onSave: handleThemeSave
2068
2790
  });
2069
2791
  break;
2070
- case "chat":
2792
+ case SCREEN.CHAT:
2071
2793
  screenContent = /* @__PURE__ */ jsx(Chat, {
2072
2794
  initialMessages: activeSession.messages,
2073
2795
  model: appConfig.model,
2074
- onCommand: handleCommand,
2796
+ onCommand: handleChatCommand,
2075
2797
  onMessagesChange: handleMessagesChange,
2076
2798
  mode,
2077
2799
  onModeChange: setMode,
2078
- sessionId: activeSession.metadata.id
2800
+ sessionId: activeSession.metadata.id,
2801
+ theme: activeTheme
2079
2802
  });
2080
2803
  break;
2081
2804
  }
@@ -2084,13 +2807,15 @@ function App({ sessionId }) {
2084
2807
  children: [
2085
2808
  /* @__PURE__ */ jsx(Header, {
2086
2809
  model: appConfig.model,
2087
- onLoad: handleHeaderLoad
2810
+ onLoad: handleHeaderLoad,
2811
+ theme: activeTheme
2088
2812
  }),
2089
2813
  isHeaderLoaded && screenContent,
2090
2814
  /* @__PURE__ */ jsx(Footer, {
2091
2815
  mode,
2092
2816
  model: appConfig.model,
2093
- onToggleMode: handleToggleMode
2817
+ onToggleMode: handleToggleMode,
2818
+ theme: activeTheme
2094
2819
  })
2095
2820
  ]
2096
2821
  });