code-ollama 0.16.0 → 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, {
@@ -1205,7 +1134,7 @@ function hasExecutablePlan(content) {
1205
1134
  }
1206
1135
  //#endregion
1207
1136
  //#region src/components/Chat/Chat.tsx
1208
- function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
1137
+ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1209
1138
  const sessionMessages = initialMessages ?? [];
1210
1139
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
1211
1140
  const [messages, setMessages] = useState(sessionMessages);
@@ -1322,13 +1251,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1322
1251
  return;
1323
1252
  }
1324
1253
  }
1325
- await prewarmCodeBlocks(assistantMessage.content);
1254
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1326
1255
  commitAssistantMessage();
1327
1256
  } catch (error) {
1328
1257
  // v8 ignore next
1329
1258
  if (!controller.signal.aborted) {
1330
1259
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1331
- await prewarmCodeBlocks(assistantMessage.content);
1260
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1332
1261
  commitAssistantMessage();
1333
1262
  }
1334
1263
  } finally {
@@ -1338,7 +1267,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1338
1267
  }, [
1339
1268
  buildToolResultMessage,
1340
1269
  model,
1341
- mode
1270
+ mode,
1271
+ theme
1342
1272
  ]);
1343
1273
  const processStreamReadOnly = useCallback(async (currentMessages) => {
1344
1274
  const controller = new AbortController();
@@ -1394,7 +1324,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1394
1324
  return;
1395
1325
  }
1396
1326
  }
1397
- await prewarmCodeBlocks(assistantMessage.content);
1327
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1398
1328
  const researchMessages = commitAssistantMessage();
1399
1329
  const planInstruction = {
1400
1330
  role: SYSTEM,
@@ -1435,7 +1365,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1435
1365
  // v8 ignore next
1436
1366
  if (!controller.signal.aborted) {
1437
1367
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1438
- await prewarmCodeBlocks(assistantMessage.content);
1368
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1439
1369
  commitAssistantMessage();
1440
1370
  }
1441
1371
  } finally {
@@ -1445,7 +1375,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1445
1375
  }, [
1446
1376
  buildPlanModeCorrectionMessage,
1447
1377
  buildToolResultMessage,
1448
- model
1378
+ model,
1379
+ theme
1449
1380
  ]);
1450
1381
  const handlePlanApproval = useCallback(async (mode) => {
1451
1382
  // v8 ignore next
@@ -1537,20 +1468,23 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1537
1468
  messages,
1538
1469
  isLoading,
1539
1470
  sessionId,
1540
- streamingMessage
1471
+ streamingMessage,
1472
+ theme
1541
1473
  }),
1542
1474
  pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
1543
1475
  planContent: pendingPlan.planContent,
1544
- onModeChange: handlePlanApproval
1476
+ onModeChange: handlePlanApproval,
1477
+ theme
1545
1478
  }),
1546
1479
  !pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
1547
1480
  toolCall: pendingToolCall,
1548
- onDecision: handleToolApproval
1481
+ onDecision: handleToolApproval,
1482
+ theme
1549
1483
  }),
1550
1484
  interruptReason && !isLoading && /* @__PURE__ */ jsx(Box, {
1551
1485
  marginBottom: 1,
1552
1486
  children: /* @__PURE__ */ jsx(Text, {
1553
- color: "red",
1487
+ color: theme.colors.error,
1554
1488
  children: interruptReason === INTERRUPT_REASON.REJECTED ? "❗ Tool call rejected." : "❗ Execution interrupted."
1555
1489
  })
1556
1490
  }),
@@ -1568,29 +1502,31 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1568
1502
  }
1569
1503
  //#endregion
1570
1504
  //#region src/components/Footer.tsx
1571
- function getModeColor(mode) {
1505
+ function getModeColor(mode, theme) {
1572
1506
  switch (mode) {
1573
- case PLAN: return "blue";
1574
- case AUTO: return "red";
1575
- 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;
1576
1510
  // v8 ignore next
1577
1511
  default: return;
1578
1512
  }
1579
1513
  }
1580
- function Footer({ mode, model, onToggleMode }) {
1514
+ function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1581
1515
  useInput((_, key) => {
1582
1516
  if (key.tab && key.shift) onToggleMode();
1583
1517
  });
1584
1518
  const modeLabel = LABEL[mode];
1519
+ const modeColor = getModeColor(mode, theme);
1585
1520
  return /* @__PURE__ */ jsx(Box, {
1586
1521
  justifyContent: "space-between",
1587
1522
  marginTop: 1,
1588
1523
  children: /* @__PURE__ */ jsxs(Text, {
1524
+ color: theme.colors.secondary,
1589
1525
  dimColor: true,
1590
1526
  children: [
1591
1527
  "Mode: ",
1592
1528
  /* @__PURE__ */ jsx(Text, {
1593
- color: getModeColor(mode),
1529
+ color: modeColor,
1594
1530
  children: modeLabel
1595
1531
  }),
1596
1532
  " (Shift+Tab to toggle)",
@@ -1598,7 +1534,7 @@ function Footer({ mode, model, onToggleMode }) {
1598
1534
  "❖",
1599
1535
  " Model: ",
1600
1536
  /* @__PURE__ */ jsx(Text, {
1601
- color: "cyan",
1537
+ color: theme.colors.model,
1602
1538
  children: model
1603
1539
  })
1604
1540
  ]
@@ -1611,7 +1547,7 @@ function abbreviatePath(dir) {
1611
1547
  const home = homedir();
1612
1548
  return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
1613
1549
  }
1614
- function Header({ model, onLoad }) {
1550
+ function Header({ model, onLoad, theme = getTheme() }) {
1615
1551
  const directory = abbreviatePath(process.cwd());
1616
1552
  useEffect(() => {
1617
1553
  onLoad();
@@ -1627,9 +1563,11 @@ function Header({ model, onLoad }) {
1627
1563
  bold: true,
1628
1564
  children: [HEADER_PREFIX, "Code Ollama"]
1629
1565
  }), /* @__PURE__ */ jsxs(Text, {
1566
+ color: theme.colors.secondary,
1630
1567
  dimColor: true,
1631
1568
  children: [
1632
- " (v",
1569
+ " ",
1570
+ "(v",
1633
1571
  VERSION,
1634
1572
  ")"
1635
1573
  ]
@@ -1638,21 +1576,24 @@ function Header({ model, onLoad }) {
1638
1576
  marginTop: 1,
1639
1577
  children: [
1640
1578
  /* @__PURE__ */ jsx(Text, {
1579
+ color: theme.colors.secondary,
1641
1580
  dimColor: true,
1642
1581
  children: "model:".padEnd(11)
1643
1582
  }),
1644
1583
  /* @__PURE__ */ jsx(Text, { children: model.padEnd(model.length + 3) }),
1645
1584
  /* @__PURE__ */ jsx(Text, {
1646
- color: "cyan",
1585
+ color: theme.colors.command,
1647
1586
  children: "/model"
1648
1587
  }),
1649
- /* @__PURE__ */ jsx(Text, {
1588
+ /* @__PURE__ */ jsxs(Text, {
1589
+ color: theme.colors.secondary,
1650
1590
  dimColor: true,
1651
- children: " to switch"
1591
+ children: [" ", "to switch"]
1652
1592
  })
1653
1593
  ]
1654
1594
  }),
1655
1595
  /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1596
+ color: theme.colors.secondary,
1656
1597
  dimColor: true,
1657
1598
  children: "directory:".padEnd(11)
1658
1599
  }), /* @__PURE__ */ jsx(Text, { children: directory })] })
@@ -1662,7 +1603,7 @@ function Header({ model, onLoad }) {
1662
1603
  }
1663
1604
  //#endregion
1664
1605
  //#region src/components/ModelPicker.tsx
1665
- function ModelPicker({ currentModel, onSelect, onClose }) {
1606
+ function ModelPicker({ currentModel, onSelect, onClose, theme = getTheme() }) {
1666
1607
  const [options, setOptions] = useState([]);
1667
1608
  const [error, setError] = useState(null);
1668
1609
  const handleChange = useCallback((model) => {
@@ -1693,7 +1634,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1693
1634
  load();
1694
1635
  }, [currentModel]);
1695
1636
  if (error) return /* @__PURE__ */ jsxs(Text, {
1696
- color: "red",
1637
+ color: theme.colors.error,
1697
1638
  children: ["Error loading models: ", error]
1698
1639
  });
1699
1640
  if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
@@ -1707,7 +1648,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1707
1648
  }
1708
1649
  //#endregion
1709
1650
  //#region src/components/SearchSettings.tsx
1710
- function SearchSettings({ currentUrl, onClose, onSave }) {
1651
+ function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
1711
1652
  const [view, setView] = useState("menu");
1712
1653
  const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1713
1654
  const [error, setError] = useState(null);
@@ -1779,10 +1720,11 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1779
1720
  placeholder: "http://localhost:8080"
1780
1721
  })] }),
1781
1722
  error && /* @__PURE__ */ jsx(Text, {
1782
- color: "red",
1723
+ color: theme.colors.error,
1783
1724
  children: error
1784
1725
  }),
1785
1726
  /* @__PURE__ */ jsx(Text, {
1727
+ color: theme.colors.secondary,
1786
1728
  dimColor: true,
1787
1729
  children: "Press Enter to save, Esc to go back."
1788
1730
  })
@@ -1793,10 +1735,14 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1793
1735
  onChange: handleChange,
1794
1736
  onCancel: onClose,
1795
1737
  children: [
1796
- /* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
1797
- color: "cyan",
1798
- children: currentUrl ?? "not set"
1799
- })] }),
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
+ ] }),
1800
1746
  /* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
1801
1747
  /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
1802
1748
  ]
@@ -1823,7 +1769,7 @@ function formatSessionLabel(session, maxWidth, prefix = "") {
1823
1769
  if (availableTitleWidth < 1) return truncate(`${prefix}${session.title}${suffix}`, maxWidth);
1824
1770
  return `${prefix}${truncate(session.title, availableTitleWidth)}${suffix}`;
1825
1771
  }
1826
- function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
1772
+ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, theme = getTheme() }) {
1827
1773
  const [view, setView] = useState("main");
1828
1774
  const [error, setError] = useState();
1829
1775
  const [, refreshSessionList] = useState(0);
@@ -1909,7 +1855,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1909
1855
  error && /* @__PURE__ */ jsx(Box, {
1910
1856
  marginBottom: 1,
1911
1857
  children: /* @__PURE__ */ jsx(Text, {
1912
- color: "red",
1858
+ color: theme.colors.error,
1913
1859
  children: error
1914
1860
  })
1915
1861
  }),
@@ -1922,25 +1868,177 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1922
1868
  });
1923
1869
  }
1924
1870
  //#endregion
1925
- //#region src/components/App.tsx
1926
- function createSession(sessionId, model) {
1927
- 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
+ });
1928
1970
  }
1929
- 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() {
1930
1984
  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);
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));
1936
2026
  const sessionRef = useRef(activeSession);
2027
+ const commandColorRef = useRef(commandColor);
2028
+ const modelRef = useRef(model);
1937
2029
  useEffect(() => {
1938
2030
  sessionRef.current = activeSession;
1939
2031
  }, [activeSession]);
2032
+ useEffect(() => {
2033
+ commandColorRef.current = commandColor;
2034
+ }, [commandColor]);
2035
+ useEffect(() => {
2036
+ modelRef.current = model;
2037
+ }, [model]);
1940
2038
  useEffect(() => {
1941
2039
  return () => {
1942
2040
  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`);
2041
+ if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, commandColorRef.current)}\n`);
1944
2042
  };
1945
2043
  }, []);
1946
2044
  const setActiveSession = useCallback((nextSession) => {
@@ -1949,73 +2047,81 @@ function App({ sessionId }) {
1949
2047
  return nextSession;
1950
2048
  });
1951
2049
  }, []);
1952
- const handleHeaderLoad = useCallback(() => {
1953
- 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);
1954
2096
  }, []);
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
- ]);
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
+ });
2019
2125
  const handleUpdateConfig = useCallback((update) => {
2020
2126
  setConfig((current) => ({
2021
2127
  ...current,
@@ -2027,10 +2133,15 @@ function App({ sessionId }) {
2027
2133
  ...current,
2028
2134
  metadata: updateSessionModel(current.metadata.id, newModel)
2029
2135
  }));
2030
- setScreen("chat");
2031
- }, []);
2032
- const handleClose = useCallback(() => {
2033
- 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);
2034
2145
  }, []);
2035
2146
  const handleToggleMode = useCallback(() => {
2036
2147
  setMode((mode) => {
@@ -2042,40 +2153,78 @@ function App({ sessionId }) {
2042
2153
  }
2043
2154
  });
2044
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]);
2045
2182
  let screenContent;
2046
2183
  switch (currentScreen) {
2047
- case "model-picker":
2184
+ case SCREEN.MODEL_PICKER:
2048
2185
  screenContent = /* @__PURE__ */ jsx(ModelPicker, {
2049
2186
  currentModel: appConfig.model,
2050
2187
  onSelect: handleUpdateConfig,
2051
- onClose: handleClose
2188
+ onClose: handleClose,
2189
+ theme: activeTheme
2052
2190
  });
2053
2191
  break;
2054
- case "search-settings":
2192
+ case SCREEN.SEARCH_SETTINGS:
2055
2193
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
2056
2194
  currentUrl: appConfig.searxngBaseUrl,
2057
2195
  onSave: handleUpdateConfig,
2058
- onClose: handleClose
2196
+ onClose: handleClose,
2197
+ theme: activeTheme
2059
2198
  });
2060
2199
  break;
2061
- case "session-manager":
2200
+ case SCREEN.SESSION_MANAGER:
2062
2201
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
2063
2202
  currentSessionId: activeSession.metadata.id,
2064
2203
  onClose: handleClose,
2065
- onDelete: handleDeleteSession,
2066
- onNew: handleCreateSession,
2067
- 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
2068
2216
  });
2069
2217
  break;
2070
- case "chat":
2218
+ case SCREEN.CHAT:
2071
2219
  screenContent = /* @__PURE__ */ jsx(Chat, {
2072
2220
  initialMessages: activeSession.messages,
2073
2221
  model: appConfig.model,
2074
- onCommand: handleCommand,
2222
+ onCommand: handleChatCommand,
2075
2223
  onMessagesChange: handleMessagesChange,
2076
2224
  mode,
2077
2225
  onModeChange: setMode,
2078
- sessionId: activeSession.metadata.id
2226
+ sessionId: activeSession.metadata.id,
2227
+ theme: activeTheme
2079
2228
  });
2080
2229
  break;
2081
2230
  }
@@ -2084,13 +2233,15 @@ function App({ sessionId }) {
2084
2233
  children: [
2085
2234
  /* @__PURE__ */ jsx(Header, {
2086
2235
  model: appConfig.model,
2087
- onLoad: handleHeaderLoad
2236
+ onLoad: handleHeaderLoad,
2237
+ theme: activeTheme
2088
2238
  }),
2089
2239
  isHeaderLoaded && screenContent,
2090
2240
  /* @__PURE__ */ jsx(Footer, {
2091
2241
  mode,
2092
2242
  model: appConfig.model,
2093
- onToggleMode: handleToggleMode
2243
+ onToggleMode: handleToggleMode,
2244
+ theme: activeTheme
2094
2245
  })
2095
2246
  ]
2096
2247
  });
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { v7 } from "uuid";
8
8
  import { exec } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
10
  //#region src/constants/command.ts
11
- var LIST = [
11
+ var LIST$1 = [
12
12
  {
13
13
  name: "/clear",
14
14
  description: "clear the current session"
@@ -21,6 +21,10 @@ var LIST = [
21
21
  name: "/model",
22
22
  description: "switch the model"
23
23
  },
24
+ {
25
+ name: "/theme",
26
+ description: "change the theme"
27
+ },
24
28
  {
25
29
  name: "/search",
26
30
  description: "configure web search"
@@ -33,7 +37,7 @@ var LIST = [
33
37
  //#endregion
34
38
  //#region package.json
35
39
  var name = "code-ollama";
36
- var version = "0.16.0";
40
+ var version = "0.17.0";
37
41
  //#endregion
38
42
  //#region src/constants/package.ts
39
43
  var NAME = name;
@@ -102,6 +106,146 @@ var USER = "user";
102
106
  var ASSISTANT = "assistant";
103
107
  var SYSTEM = "system";
104
108
  //#endregion
109
+ //#region src/constants/theme.ts
110
+ var DEFAULT_THEME_ID = "github-dark";
111
+ var LIST = [
112
+ {
113
+ id: "github-light",
114
+ label: "GitHub Light",
115
+ description: "Light GitHub palette",
116
+ markdownTheme: "gitHub",
117
+ codeTheme: "github-light",
118
+ colors: {
119
+ accent: "blue",
120
+ border: "blue",
121
+ codeBorder: "blue",
122
+ command: "blue",
123
+ error: "red",
124
+ messageSystem: "gray",
125
+ modeAuto: "magenta",
126
+ modePlan: "blue",
127
+ modeSafe: "green",
128
+ model: "blue",
129
+ secondary: "gray",
130
+ status: "cyan",
131
+ warning: "yellow"
132
+ }
133
+ },
134
+ {
135
+ id: "github-dark",
136
+ label: "GitHub Dark",
137
+ description: "Dark GitHub palette",
138
+ markdownTheme: "gitHub",
139
+ codeTheme: "github-dark",
140
+ colors: {
141
+ accent: "cyan",
142
+ border: "cyan",
143
+ codeBorder: "gray",
144
+ command: "cyan",
145
+ error: "red",
146
+ messageSystem: "gray",
147
+ modeAuto: "magenta",
148
+ modePlan: "blue",
149
+ modeSafe: "green",
150
+ model: "cyan",
151
+ secondary: "gray",
152
+ status: "cyan",
153
+ warning: "yellow"
154
+ }
155
+ },
156
+ {
157
+ id: "nord",
158
+ label: "Nord",
159
+ description: "Cool nordic blues",
160
+ markdownTheme: "nord",
161
+ codeTheme: "nord",
162
+ colors: {
163
+ accent: "cyan",
164
+ border: "blue",
165
+ codeBorder: "blue",
166
+ command: "cyan",
167
+ error: "red",
168
+ messageSystem: "gray",
169
+ modeAuto: "magenta",
170
+ modePlan: "blue",
171
+ modeSafe: "green",
172
+ model: "cyan",
173
+ secondary: "gray",
174
+ status: "blue",
175
+ warning: "yellow"
176
+ }
177
+ },
178
+ {
179
+ id: "dracula",
180
+ label: "Dracula",
181
+ description: "Bright neon contrast",
182
+ markdownTheme: "dracula",
183
+ codeTheme: "dracula",
184
+ colors: {
185
+ accent: "magenta",
186
+ border: "magenta",
187
+ codeBorder: "magenta",
188
+ command: "magenta",
189
+ error: "red",
190
+ messageSystem: "gray",
191
+ modeAuto: "yellow",
192
+ modePlan: "magenta",
193
+ modeSafe: "green",
194
+ model: "magenta",
195
+ secondary: "gray",
196
+ status: "cyan",
197
+ warning: "yellow"
198
+ }
199
+ },
200
+ {
201
+ id: "solarized-light",
202
+ label: "Solarized Light",
203
+ description: "Warm light contrast",
204
+ markdownTheme: "solarized",
205
+ codeTheme: "solarized-light",
206
+ colors: {
207
+ accent: "yellow",
208
+ border: "yellow",
209
+ codeBorder: "yellow",
210
+ command: "yellow",
211
+ error: "red",
212
+ messageSystem: "gray",
213
+ modeAuto: "cyan",
214
+ modePlan: "blue",
215
+ modeSafe: "green",
216
+ model: "yellow",
217
+ secondary: "gray",
218
+ status: "cyan",
219
+ warning: "yellow"
220
+ }
221
+ },
222
+ {
223
+ id: "solarized-dark",
224
+ label: "Solarized Dark",
225
+ description: "Warm dark contrast",
226
+ markdownTheme: "solarized",
227
+ codeTheme: "solarized-dark",
228
+ colors: {
229
+ accent: "yellow",
230
+ border: "yellow",
231
+ codeBorder: "yellow",
232
+ command: "yellow",
233
+ error: "red",
234
+ messageSystem: "gray",
235
+ modeAuto: "cyan",
236
+ modePlan: "blue",
237
+ modeSafe: "green",
238
+ model: "yellow",
239
+ secondary: "gray",
240
+ status: "cyan",
241
+ warning: "yellow"
242
+ }
243
+ }
244
+ ];
245
+ function getTheme(themeId = DEFAULT_THEME_ID) {
246
+ return LIST.find(({ id }) => id === themeId) ?? LIST[0];
247
+ }
248
+ //#endregion
105
249
  //#region src/constants/tool.ts
106
250
  var READ_FILE = "read_file";
107
251
  var WRITE_FILE = "write_file";
@@ -166,7 +310,8 @@ function loadConfig() {
166
310
  return {
167
311
  host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULT_HOST,
168
312
  model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULT_MODEL$1,
169
- searxngBaseUrl: file.searxngBaseUrl
313
+ searxngBaseUrl: file.searxngBaseUrl,
314
+ theme: file.theme ?? "github-dark"
170
315
  };
171
316
  }
172
317
  function saveConfig(patch) {
@@ -370,7 +515,16 @@ function deleteSession(id) {
370
515
  }
371
516
  //#endregion
372
517
  //#region src/utils/terminal.ts
373
- var ANSI_COLOR = { cyan: ["\x1B[36m", "\x1B[39m"] };
518
+ var ANSI_RESET_FOREGROUND = "\x1B[39m";
519
+ var ANSI_COLOR = {
520
+ blue: ["\x1B[34m", ANSI_RESET_FOREGROUND],
521
+ cyan: ["\x1B[36m", ANSI_RESET_FOREGROUND],
522
+ gray: ["\x1B[90m", ANSI_RESET_FOREGROUND],
523
+ green: ["\x1B[32m", ANSI_RESET_FOREGROUND],
524
+ magenta: ["\x1B[35m", ANSI_RESET_FOREGROUND],
525
+ red: ["\x1B[31m", ANSI_RESET_FOREGROUND],
526
+ yellow: ["\x1B[33m", ANSI_RESET_FOREGROUND]
527
+ };
374
528
  function color(text, name) {
375
529
  const [open, close] = ANSI_COLOR[name];
376
530
  return `${open}${text}${close}`;
@@ -931,7 +1085,7 @@ async function main(args = process.argv.slice(2)) {
931
1085
  else await launchTui();
932
1086
  }
933
1087
  async function launchTui(sessionId) {
934
- const { renderApp } = await import("./assets/tui-85A3pZD2.js");
1088
+ const { renderApp } = await import("./assets/tui-CCHcdC7V.js");
935
1089
  reset();
936
1090
  renderApp(sessionId);
937
1091
  }
@@ -947,4 +1101,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
947
1101
  if (isEntrypoint()) main();
948
1102
  // v8 ignore stop
949
1103
  //#endregion
950
- export { LABEL as A, withSystemMessage as C, USER as D, SYSTEM as E, VERSION as F, LIST as I, SAFE as M, APPROVE as N, PLAN_GENERATION_INSTRUCTION as O, REJECT as P, resetSystemMessage as S, ASSISTANT as T, setClearHandler as _, tick as a, loadConfig as b, appendMessage as c, deleteSessionIfEmpty as d, listSessions as f, reset as g, clear as h, WRITE_TOOLS as i, PLAN as j, AUTO as k, createSession as l, updateSessionModel as m, main, READ_TOOLS as n, color as o, loadSession as p, TOOLS as r, write as s, executeTool as t, deleteSession as u, listModels as v, HEADER_PREFIX as w, saveConfig as x, streamChat as y };
1104
+ export { PLAN_GENERATION_INSTRUCTION as A, withSystemMessage as C, ASSISTANT as D, getTheme as E, APPROVE as F, REJECT as I, VERSION as L, LABEL as M, PLAN as N, SYSTEM as O, SAFE as P, LIST$1 as R, resetSystemMessage as S, LIST as T, setClearHandler as _, tick as a, loadConfig as b, appendMessage as c, deleteSessionIfEmpty as d, listSessions as f, reset as g, clear as h, WRITE_TOOLS as i, AUTO as j, USER as k, createSession as l, updateSessionModel as m, main, READ_TOOLS as n, color as o, loadSession as p, TOOLS as r, write as s, executeTool as t, deleteSession as u, listModels as v, HEADER_PREFIX as w, saveConfig as x, streamChat as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Ollama coding agent that runs in your terminal",
5
5
  "author": "Mark <mark@remarkablemark.org> (https://remarkablemark.org)",
6
6
  "type": "module",
@@ -52,7 +52,6 @@
52
52
  "devDependencies": {
53
53
  "@commitlint/cli": "21.0.1",
54
54
  "@commitlint/config-conventional": "21.0.1",
55
- "@eslint/config-helpers": "0.6.0",
56
55
  "@eslint/js": "10.0.1",
57
56
  "@types/node": "25.8.0",
58
57
  "@types/react": "19.2.14",
@@ -63,7 +62,7 @@
63
62
  "globals": "17.6.0",
64
63
  "husky": "9.1.7",
65
64
  "ink-testing-library": "4.0.0",
66
- "lint-staged": "17.0.4",
65
+ "lint-staged": "17.0.5",
67
66
  "prettier": "3.8.3",
68
67
  "publint": "0.3.21",
69
68
  "tsx": "4.22.0",