codemaxxing 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  > your code. your model. no excuses.
4
4
 
5
+ <p align="center">
6
+ <img src="assets/screenshot.jpg" alt="codemaxxing terminal UI" width="700">
7
+ </p>
8
+
5
9
  Open-source terminal coding agent. Connect **any** LLM — local or remote — and start building. Like Claude Code, but you bring your own model.
6
10
 
7
11
  ## Why?
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listMode
9
9
  import { listSessions, getSession, loadMessages } from "./utils/sessions.js";
10
10
  import { execSync } from "child_process";
11
11
  import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
12
+ import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
12
13
  const VERSION = "0.1.0";
13
14
  // ── Helpers ──
14
15
  function formatTimeAgo(date) {
@@ -37,6 +38,7 @@ const SLASH_COMMANDS = [
37
38
  { cmd: "/git on", desc: "enable auto-commits" },
38
39
  { cmd: "/git off", desc: "disable auto-commits" },
39
40
  { cmd: "/models", desc: "list available models" },
41
+ { cmd: "/theme", desc: "switch color theme" },
40
42
  { cmd: "/model", desc: "switch model mid-session" },
41
43
  { cmd: "/sessions", desc: "list past sessions" },
42
44
  { cmd: "/resume", desc: "resume a past session" },
@@ -51,7 +53,7 @@ const SPINNER_MESSAGES = [
51
53
  "Hacking the main frame...", "Codemaxxing...", "Vibe coding...", "Running a marathon...",
52
54
  ];
53
55
  // ── Neon Spinner ──
54
- function NeonSpinner({ message }) {
56
+ function NeonSpinner({ message, colors }) {
55
57
  const [frame, setFrame] = useState(0);
56
58
  const [elapsed, setElapsed] = useState(0);
57
59
  useEffect(() => {
@@ -62,11 +64,11 @@ function NeonSpinner({ message }) {
62
64
  }, 80);
63
65
  return () => clearInterval(interval);
64
66
  }, []);
65
- return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "#00FFFF", children: SPINNER_FRAMES[frame] }), " ", _jsx(Text, { bold: true, color: "#FF00FF", children: message }), " ", _jsxs(Text, { color: "#008B8B", children: ["[", elapsed, "s]"] })] }));
67
+ return (_jsxs(Text, { children: [" ", _jsx(Text, { color: colors.spinner, children: SPINNER_FRAMES[frame] }), " ", _jsx(Text, { bold: true, color: colors.secondary, children: message }), " ", _jsxs(Text, { color: colors.muted, children: ["[", elapsed, "s]"] })] }));
66
68
  }
67
69
  // ── Streaming Indicator (subtle, shows model is still working) ──
68
70
  const STREAM_DOTS = ["· ", "·· ", "···", " ··", " ·", " "];
69
- function StreamingIndicator() {
71
+ function StreamingIndicator({ colors }) {
70
72
  const [frame, setFrame] = useState(0);
71
73
  useEffect(() => {
72
74
  const interval = setInterval(() => {
@@ -74,7 +76,7 @@ function StreamingIndicator() {
74
76
  }, 300);
75
77
  return () => clearInterval(interval);
76
78
  }, []);
77
- return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: "#008B8B", children: STREAM_DOTS[frame] }), " ", _jsx(Text, { color: "#008B8B", children: "streaming" })] }));
79
+ return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: colors.spinner, children: STREAM_DOTS[frame] }), " ", _jsx(Text, { color: colors.muted, children: "streaming" })] }));
78
80
  }
79
81
  let msgId = 0;
80
82
  // ── Main App ──
@@ -91,6 +93,7 @@ function App() {
91
93
  const [spinnerMsg, setSpinnerMsg] = useState("");
92
94
  const [agent, setAgent] = useState(null);
93
95
  const [modelName, setModelName] = useState("");
96
+ const [theme, setTheme] = useState(getTheme(DEFAULT_THEME));
94
97
  const providerRef = React.useRef({ baseUrl: "", apiKey: "" });
95
98
  const [ready, setReady] = useState(false);
96
99
  const [connectionInfo, setConnectionInfo] = useState([]);
@@ -99,6 +102,8 @@ function App() {
99
102
  const [inputKey, setInputKey] = useState(0);
100
103
  const [sessionPicker, setSessionPicker] = useState(null);
101
104
  const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
105
+ const [themePicker, setThemePicker] = useState(false);
106
+ const [themePickerIndex, setThemePickerIndex] = useState(0);
102
107
  const [approval, setApproval] = useState(null);
103
108
  // Listen for paste events from stdin interceptor
104
109
  useEffect(() => {
@@ -323,6 +328,23 @@ function App() {
323
328
  addMsg("info", `✅ Switched to model: ${newModel}`);
324
329
  return;
325
330
  }
331
+ if (trimmed.startsWith("/theme")) {
332
+ const themeName = trimmed.replace("/theme", "").trim();
333
+ if (!themeName) {
334
+ const themeKeys = listThemes();
335
+ const currentIdx = themeKeys.indexOf(theme.name.toLowerCase());
336
+ setThemePicker(true);
337
+ setThemePickerIndex(currentIdx >= 0 ? currentIdx : 0);
338
+ return;
339
+ }
340
+ if (!THEMES[themeName]) {
341
+ addMsg("error", `Theme "${themeName}" not found. Use /theme to see available themes.`);
342
+ return;
343
+ }
344
+ setTheme(getTheme(themeName));
345
+ addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
346
+ return;
347
+ }
326
348
  if (trimmed === "/map") {
327
349
  const map = agent.getRepoMap();
328
350
  if (map) {
@@ -465,6 +487,30 @@ function App() {
465
487
  return;
466
488
  }
467
489
  }
490
+ // Theme picker navigation
491
+ if (themePicker) {
492
+ const themeKeys = listThemes();
493
+ if (key.upArrow) {
494
+ setThemePickerIndex((prev) => (prev - 1 + themeKeys.length) % themeKeys.length);
495
+ return;
496
+ }
497
+ if (key.downArrow) {
498
+ setThemePickerIndex((prev) => (prev + 1) % themeKeys.length);
499
+ return;
500
+ }
501
+ if (key.return) {
502
+ const selected = themeKeys[themePickerIndex];
503
+ setTheme(getTheme(selected));
504
+ setThemePicker(false);
505
+ addMsg("info", `✅ Switched to theme: ${THEMES[selected].name}`);
506
+ return;
507
+ }
508
+ if (key.escape) {
509
+ setThemePicker(false);
510
+ return;
511
+ }
512
+ return;
513
+ }
468
514
  // Session picker navigation
469
515
  if (sessionPicker) {
470
516
  if (key.upArrow) {
@@ -576,27 +622,27 @@ function App() {
576
622
  "| | | | | | | | / .'. \\ / .'. \\ | |'->| | \\ | | '-' | ",
577
623
  "`--' `--' `--' `--'`--' '--'`--' '--'`--' `--' `--' `-----' ",
578
624
  ];
579
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00FFFF", paddingX: 1, children: [codeLines.map((line, i) => (_jsx(Text, { color: "#00FFFF", children: line }, `c${i}`))), maxxingLines.map((line, i) => (_jsx(Text, { color: i === maxxingLines.length - 1 ? "#CC00CC" : "#FF00FF", children: line }, `m${i}`))), _jsxs(Text, { children: [_jsx(Text, { color: "#008B8B", children: " v" + VERSION }), " ", _jsx(Text, { color: "#00FFFF", children: "\uD83D\uDCAA" }), " ", _jsx(Text, { dimColor: true, children: "your code. your model. no excuses." })] }), _jsxs(Text, { dimColor: true, children: [" Type ", _jsx(Text, { color: "#008B8B", children: "/help" }), " for commands · ", _jsx(Text, { color: "#008B8B", children: "Ctrl+C" }), " twice to exit"] })] }), connectionInfo.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#008B8B", paddingX: 1, marginBottom: 1, children: connectionInfo.map((line, i) => (_jsx(Text, { color: line.startsWith("✔") ? "#00FFFF" : line.startsWith("✗") ? "red" : "#008B8B", children: line }, i))) })), messages.map((msg) => {
625
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.border, paddingX: 1, children: [codeLines.map((line, i) => (_jsx(Text, { color: theme.colors.primary, children: line }, `c${i}`))), maxxingLines.map((line, i) => (_jsx(Text, { color: theme.colors.secondary, children: line }, `m${i}`))), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.muted, children: " v" + VERSION }), " ", _jsx(Text, { color: theme.colors.primary, children: "\uD83D\uDCAA" }), " ", _jsx(Text, { dimColor: true, children: "your code. your model. no excuses." })] }), _jsxs(Text, { dimColor: true, children: [" Type ", _jsx(Text, { color: theme.colors.muted, children: "/help" }), " for commands · ", _jsx(Text, { color: theme.colors.muted, children: "Ctrl+C" }), " twice to exit"] })] }), connectionInfo.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 1, children: connectionInfo.map((line, i) => (_jsx(Text, { color: line.startsWith("✔") ? theme.colors.primary : line.startsWith("✗") ? theme.colors.error : theme.colors.muted, children: line }, i))) })), messages.map((msg) => {
580
626
  switch (msg.type) {
581
627
  case "user":
582
- return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "#008B8B", children: [" > ", msg.text] }) }, msg.id));
628
+ return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.userInput, children: [" > ", msg.text] }) }, msg.id));
583
629
  case "response":
584
- return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: msg.text.split("\n").map((l, i) => (_jsxs(Text, { wrap: "wrap", children: [i === 0 ? _jsx(Text, { color: "#00FFFF", children: "\u25CF " }) : _jsx(Text, { children: " " }), l.startsWith("```") ? _jsx(Text, { color: "#008B8B", children: l }) :
585
- l.startsWith("# ") || l.startsWith("## ") ? _jsx(Text, { bold: true, color: "#FF00FF", children: l }) :
630
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: msg.text.split("\n").map((l, i) => (_jsxs(Text, { wrap: "wrap", children: [i === 0 ? _jsx(Text, { color: theme.colors.response, children: "\u25CF " }) : _jsx(Text, { children: " " }), l.startsWith("```") ? _jsx(Text, { color: theme.colors.muted, children: l }) :
631
+ l.startsWith("# ") || l.startsWith("## ") ? _jsx(Text, { bold: true, color: theme.colors.secondary, children: l }) :
586
632
  l.startsWith("**") ? _jsx(Text, { bold: true, children: l }) :
587
633
  _jsx(Text, { children: l })] }, i))) }, msg.id));
588
634
  case "tool":
589
- return (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "#00FFFF", children: " \u25CF " }), _jsx(Text, { bold: true, color: "#FF00FF", children: msg.text })] }) }, msg.id));
635
+ return (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.response, children: " \u25CF " }), _jsx(Text, { bold: true, color: theme.colors.tool, children: msg.text })] }) }, msg.id));
590
636
  case "tool-result":
591
- return _jsxs(Text, { color: "#008B8B", children: [" ", msg.text] }, msg.id);
637
+ return _jsxs(Text, { color: theme.colors.toolResult, children: [" ", msg.text] }, msg.id);
592
638
  case "error":
593
- return _jsxs(Text, { color: "red", children: [" ", msg.text] }, msg.id);
639
+ return _jsxs(Text, { color: theme.colors.error, children: [" ", msg.text] }, msg.id);
594
640
  case "info":
595
- return _jsxs(Text, { color: "#008B8B", children: [" ", msg.text] }, msg.id);
641
+ return _jsxs(Text, { color: theme.colors.muted, children: [" ", msg.text] }, msg.id);
596
642
  default:
597
643
  return _jsx(Text, { children: msg.text }, msg.id);
598
644
  }
599
- }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg }), streaming && !loading && _jsx(StreamingIndicator, {}), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#FF8C00", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: "#FF8C00", children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: "#008B8B", children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: "#008B8B", children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: "#008B8B", children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: "#00FF00", bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: "#FF0000", bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: "#00FFFF", bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#FF00FF", paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: "#FF00FF", children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: "#FF00FF", bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? "#FF00FF" : "#008B8B", children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#008B8B", paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: "#FF00FF", bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? "#FF00FF" : "#00FFFF", bold: true, children: c.cmd }), _jsxs(Text, { color: "#008B8B", children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? "#FF8C00" : "#00FFFF", paddingX: 1, children: [_jsx(Text, { color: "#FF00FF", bold: true, children: "> " }), approval ? (_jsx(Text, { color: "#FF8C00", children: "waiting for approval..." })) : ready && !loading ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: "#008B8B", children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(v); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
645
+ }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: theme.colors.muted, children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: theme.colors.muted, children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: theme.colors.muted, children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(v); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
600
646
  const tokens = agent.estimateTokens();
601
647
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
602
648
  })(), " tokens", modelName ? ` · 🤖 ${modelName}` : ""] }) }))] }));
@@ -0,0 +1,24 @@
1
+ export interface Theme {
2
+ name: string;
3
+ description: string;
4
+ colors: {
5
+ primary: string;
6
+ secondary: string;
7
+ muted: string;
8
+ text: string;
9
+ userInput: string;
10
+ response: string;
11
+ tool: string;
12
+ toolResult: string;
13
+ error: string;
14
+ success: string;
15
+ warning: string;
16
+ spinner: string;
17
+ border: string;
18
+ suggestion: string;
19
+ };
20
+ }
21
+ export declare const THEMES: Record<string, Theme>;
22
+ export declare const DEFAULT_THEME = "neon";
23
+ export declare function getTheme(name: string): Theme;
24
+ export declare function listThemes(): string[];
package/dist/themes.js ADDED
@@ -0,0 +1,149 @@
1
+ export const THEMES = {
2
+ neon: {
3
+ name: "Neon",
4
+ description: "Cyberpunk neon (default)",
5
+ colors: {
6
+ primary: "#00FFFF",
7
+ secondary: "#FF00FF",
8
+ muted: "#008B8B",
9
+ text: "",
10
+ userInput: "#008B8B",
11
+ response: "#00FFFF",
12
+ tool: "#FF00FF",
13
+ toolResult: "#008B8B",
14
+ error: "red",
15
+ success: "#00FF00",
16
+ warning: "#FF8C00",
17
+ spinner: "#00FFFF",
18
+ border: "#00FFFF",
19
+ suggestion: "#FF00FF",
20
+ },
21
+ },
22
+ dracula: {
23
+ name: "Dracula",
24
+ description: "Dark purple tones",
25
+ colors: {
26
+ primary: "#BD93F9",
27
+ secondary: "#FF79C6",
28
+ muted: "#6272A4",
29
+ text: "#F8F8F2",
30
+ userInput: "#8BE9FD",
31
+ response: "#BD93F9",
32
+ tool: "#FF79C6",
33
+ toolResult: "#6272A4",
34
+ error: "#FF5555",
35
+ success: "#50FA7B",
36
+ warning: "#FFB86C",
37
+ spinner: "#BD93F9",
38
+ border: "#BD93F9",
39
+ suggestion: "#FF79C6",
40
+ },
41
+ },
42
+ gruvbox: {
43
+ name: "Gruvbox",
44
+ description: "Warm retro tones",
45
+ colors: {
46
+ primary: "#FE8019",
47
+ secondary: "#FABD2F",
48
+ muted: "#928374",
49
+ text: "#EBDBB2",
50
+ userInput: "#83A598",
51
+ response: "#FE8019",
52
+ tool: "#FABD2F",
53
+ toolResult: "#928374",
54
+ error: "#FB4934",
55
+ success: "#B8BB26",
56
+ warning: "#FABD2F",
57
+ spinner: "#FE8019",
58
+ border: "#FE8019",
59
+ suggestion: "#FABD2F",
60
+ },
61
+ },
62
+ nord: {
63
+ name: "Nord",
64
+ description: "Cool arctic blues",
65
+ colors: {
66
+ primary: "#88C0D0",
67
+ secondary: "#81A1C1",
68
+ muted: "#4C566A",
69
+ text: "#ECEFF4",
70
+ userInput: "#88C0D0",
71
+ response: "#81A1C1",
72
+ tool: "#5E81AC",
73
+ toolResult: "#4C566A",
74
+ error: "#BF616A",
75
+ success: "#A3BE8C",
76
+ warning: "#EBCB8B",
77
+ spinner: "#88C0D0",
78
+ border: "#81A1C1",
79
+ suggestion: "#88C0D0",
80
+ },
81
+ },
82
+ mono: {
83
+ name: "Mono",
84
+ description: "Clean monochrome — easy on the eyes",
85
+ colors: {
86
+ primary: "#AAAAAA",
87
+ secondary: "#FFFFFF",
88
+ muted: "#666666",
89
+ text: "#CCCCCC",
90
+ userInput: "#AAAAAA",
91
+ response: "#FFFFFF",
92
+ tool: "#CCCCCC",
93
+ toolResult: "#666666",
94
+ error: "#FF6666",
95
+ success: "#66FF66",
96
+ warning: "#FFAA66",
97
+ spinner: "#AAAAAA",
98
+ border: "#888888",
99
+ suggestion: "#FFFFFF",
100
+ },
101
+ },
102
+ solarized: {
103
+ name: "Solarized",
104
+ description: "Solarized dark",
105
+ colors: {
106
+ primary: "#268BD2",
107
+ secondary: "#2AA198",
108
+ muted: "#586E75",
109
+ text: "#839496",
110
+ userInput: "#2AA198",
111
+ response: "#268BD2",
112
+ tool: "#B58900",
113
+ toolResult: "#586E75",
114
+ error: "#DC322F",
115
+ success: "#859900",
116
+ warning: "#CB4B16",
117
+ spinner: "#268BD2",
118
+ border: "#268BD2",
119
+ suggestion: "#2AA198",
120
+ },
121
+ },
122
+ hacker: {
123
+ name: "Hacker",
124
+ description: "Green on black — classic terminal",
125
+ colors: {
126
+ primary: "#00FF00",
127
+ secondary: "#00CC00",
128
+ muted: "#006600",
129
+ text: "#00DD00",
130
+ userInput: "#00FF00",
131
+ response: "#00FF00",
132
+ tool: "#00CC00",
133
+ toolResult: "#006600",
134
+ error: "#FF0000",
135
+ success: "#00FF00",
136
+ warning: "#FFFF00",
137
+ spinner: "#00FF00",
138
+ border: "#00FF00",
139
+ suggestion: "#00CC00",
140
+ },
141
+ },
142
+ };
143
+ export const DEFAULT_THEME = "neon";
144
+ export function getTheme(name) {
145
+ return THEMES[name] ?? THEMES[DEFAULT_THEME];
146
+ }
147
+ export function listThemes() {
148
+ return Object.keys(THEMES);
149
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.tsx CHANGED
@@ -9,6 +9,7 @@ import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listMode
9
9
  import { listSessions, getSession, loadMessages } from "./utils/sessions.js";
10
10
  import { execSync } from "child_process";
11
11
  import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
12
+ import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./themes.js";
12
13
 
13
14
  const VERSION = "0.1.0";
14
15
 
@@ -37,6 +38,7 @@ const SLASH_COMMANDS = [
37
38
  { cmd: "/git on", desc: "enable auto-commits" },
38
39
  { cmd: "/git off", desc: "disable auto-commits" },
39
40
  { cmd: "/models", desc: "list available models" },
41
+ { cmd: "/theme", desc: "switch color theme" },
40
42
  { cmd: "/model", desc: "switch model mid-session" },
41
43
  { cmd: "/sessions", desc: "list past sessions" },
42
44
  { cmd: "/resume", desc: "resume a past session" },
@@ -54,7 +56,7 @@ const SPINNER_MESSAGES = [
54
56
  ];
55
57
 
56
58
  // ── Neon Spinner ──
57
- function NeonSpinner({ message }: { message: string }) {
59
+ function NeonSpinner({ message, colors }: { message: string; colors: Theme['colors'] }) {
58
60
  const [frame, setFrame] = useState(0);
59
61
  const [elapsed, setElapsed] = useState(0);
60
62
 
@@ -69,16 +71,16 @@ function NeonSpinner({ message }: { message: string }) {
69
71
 
70
72
  return (
71
73
  <Text>
72
- {" "}<Text color="#00FFFF">{SPINNER_FRAMES[frame]}</Text>
73
- {" "}<Text bold color="#FF00FF">{message}</Text>
74
- {" "}<Text color="#008B8B">[{elapsed}s]</Text>
74
+ {" "}<Text color={colors.spinner}>{SPINNER_FRAMES[frame]}</Text>
75
+ {" "}<Text bold color={colors.secondary}>{message}</Text>
76
+ {" "}<Text color={colors.muted}>[{elapsed}s]</Text>
75
77
  </Text>
76
78
  );
77
79
  }
78
80
 
79
81
  // ── Streaming Indicator (subtle, shows model is still working) ──
80
82
  const STREAM_DOTS = ["· ", "·· ", "···", " ··", " ·", " "];
81
- function StreamingIndicator() {
83
+ function StreamingIndicator({ colors }: { colors: Theme['colors'] }) {
82
84
  const [frame, setFrame] = useState(0);
83
85
 
84
86
  useEffect(() => {
@@ -90,8 +92,8 @@ function StreamingIndicator() {
90
92
 
91
93
  return (
92
94
  <Text dimColor>
93
- {" "}<Text color="#008B8B">{STREAM_DOTS[frame]}</Text>
94
- {" "}<Text color="#008B8B">streaming</Text>
95
+ {" "}<Text color={colors.spinner}>{STREAM_DOTS[frame]}</Text>
96
+ {" "}<Text color={colors.muted}>streaming</Text>
95
97
  </Text>
96
98
  );
97
99
  }
@@ -120,6 +122,7 @@ function App() {
120
122
  const [spinnerMsg, setSpinnerMsg] = useState("");
121
123
  const [agent, setAgent] = useState<CodingAgent | null>(null);
122
124
  const [modelName, setModelName] = useState("");
125
+ const [theme, setTheme] = useState<Theme>(getTheme(DEFAULT_THEME));
123
126
  const providerRef = React.useRef<{ baseUrl: string; apiKey: string }>({ baseUrl: "", apiKey: "" });
124
127
  const [ready, setReady] = useState(false);
125
128
  const [connectionInfo, setConnectionInfo] = useState<string[]>([]);
@@ -128,6 +131,8 @@ function App() {
128
131
  const [inputKey, setInputKey] = useState(0);
129
132
  const [sessionPicker, setSessionPicker] = useState<Array<{ id: string; display: string }> | null>(null);
130
133
  const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
134
+ const [themePicker, setThemePicker] = useState(false);
135
+ const [themePickerIndex, setThemePickerIndex] = useState(0);
131
136
  const [approval, setApproval] = useState<{
132
137
  tool: string;
133
138
  args: Record<string, unknown>;
@@ -370,6 +375,23 @@ function App() {
370
375
  addMsg("info", `✅ Switched to model: ${newModel}`);
371
376
  return;
372
377
  }
378
+ if (trimmed.startsWith("/theme")) {
379
+ const themeName = trimmed.replace("/theme", "").trim();
380
+ if (!themeName) {
381
+ const themeKeys = listThemes();
382
+ const currentIdx = themeKeys.indexOf(theme.name.toLowerCase());
383
+ setThemePicker(true);
384
+ setThemePickerIndex(currentIdx >= 0 ? currentIdx : 0);
385
+ return;
386
+ }
387
+ if (!THEMES[themeName]) {
388
+ addMsg("error", `Theme "${themeName}" not found. Use /theme to see available themes.`);
389
+ return;
390
+ }
391
+ setTheme(getTheme(themeName));
392
+ addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
393
+ return;
394
+ }
373
395
  if (trimmed === "/map") {
374
396
  const map = agent.getRepoMap();
375
397
  if (map) {
@@ -511,6 +533,31 @@ function App() {
511
533
  }
512
534
  }
513
535
 
536
+ // Theme picker navigation
537
+ if (themePicker) {
538
+ const themeKeys = listThemes();
539
+ if (key.upArrow) {
540
+ setThemePickerIndex((prev) => (prev - 1 + themeKeys.length) % themeKeys.length);
541
+ return;
542
+ }
543
+ if (key.downArrow) {
544
+ setThemePickerIndex((prev) => (prev + 1) % themeKeys.length);
545
+ return;
546
+ }
547
+ if (key.return) {
548
+ const selected = themeKeys[themePickerIndex];
549
+ setTheme(getTheme(selected));
550
+ setThemePicker(false);
551
+ addMsg("info", `✅ Switched to theme: ${THEMES[selected].name}`);
552
+ return;
553
+ }
554
+ if (key.escape) {
555
+ setThemePicker(false);
556
+ return;
557
+ }
558
+ return;
559
+ }
560
+
514
561
  // Session picker navigation
515
562
  if (sessionPicker) {
516
563
  if (key.upArrow) {
@@ -628,26 +675,26 @@ function App() {
628
675
  return (
629
676
  <Box flexDirection="column">
630
677
  {/* ═══ BANNER BOX ═══ */}
631
- <Box flexDirection="column" borderStyle="round" borderColor="#00FFFF" paddingX={1}>
678
+ <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.border} paddingX={1}>
632
679
  {codeLines.map((line, i) => (
633
- <Text key={`c${i}`} color="#00FFFF">{line}</Text>
680
+ <Text key={`c${i}`} color={theme.colors.primary}>{line}</Text>
634
681
  ))}
635
682
  {maxxingLines.map((line, i) => (
636
- <Text key={`m${i}`} color={i === maxxingLines.length - 1 ? "#CC00CC" : "#FF00FF"}>{line}</Text>
683
+ <Text key={`m${i}`} color={theme.colors.secondary}>{line}</Text>
637
684
  ))}
638
685
  <Text>
639
- <Text color="#008B8B">{" v" + VERSION}</Text>
640
- {" "}<Text color="#00FFFF">💪</Text>
686
+ <Text color={theme.colors.muted}>{" v" + VERSION}</Text>
687
+ {" "}<Text color={theme.colors.primary}>💪</Text>
641
688
  {" "}<Text dimColor>your code. your model. no excuses.</Text>
642
689
  </Text>
643
- <Text dimColor>{" Type "}<Text color="#008B8B">/help</Text>{" for commands · "}<Text color="#008B8B">Ctrl+C</Text>{" twice to exit"}</Text>
690
+ <Text dimColor>{" Type "}<Text color={theme.colors.muted}>/help</Text>{" for commands · "}<Text color={theme.colors.muted}>Ctrl+C</Text>{" twice to exit"}</Text>
644
691
  </Box>
645
692
 
646
693
  {/* ═══ CONNECTION INFO BOX ═══ */}
647
694
  {connectionInfo.length > 0 && (
648
- <Box flexDirection="column" borderStyle="single" borderColor="#008B8B" paddingX={1} marginBottom={1}>
695
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.muted} paddingX={1} marginBottom={1}>
649
696
  {connectionInfo.map((line, i) => (
650
- <Text key={i} color={line.startsWith("✔") ? "#00FFFF" : line.startsWith("✗") ? "red" : "#008B8B"}>{line}</Text>
697
+ <Text key={i} color={line.startsWith("✔") ? theme.colors.primary : line.startsWith("✗") ? theme.colors.error : theme.colors.muted}>{line}</Text>
651
698
  ))}
652
699
  </Box>
653
700
  )}
@@ -658,7 +705,7 @@ function App() {
658
705
  case "user":
659
706
  return (
660
707
  <Box key={msg.id} marginTop={1}>
661
- <Text color="#008B8B">{" > "}{msg.text}</Text>
708
+ <Text color={theme.colors.userInput}>{" > "}{msg.text}</Text>
662
709
  </Box>
663
710
  );
664
711
  case "response":
@@ -666,9 +713,9 @@ function App() {
666
713
  <Box key={msg.id} flexDirection="column" marginLeft={2} marginBottom={1}>
667
714
  {msg.text.split("\n").map((l, i) => (
668
715
  <Text key={i} wrap="wrap">
669
- {i === 0 ? <Text color="#00FFFF">● </Text> : <Text> </Text>}
670
- {l.startsWith("```") ? <Text color="#008B8B">{l}</Text> :
671
- l.startsWith("# ") || l.startsWith("## ") ? <Text bold color="#FF00FF">{l}</Text> :
716
+ {i === 0 ? <Text color={theme.colors.response}>● </Text> : <Text> </Text>}
717
+ {l.startsWith("```") ? <Text color={theme.colors.muted}>{l}</Text> :
718
+ l.startsWith("# ") || l.startsWith("## ") ? <Text bold color={theme.colors.secondary}>{l}</Text> :
672
719
  l.startsWith("**") ? <Text bold>{l}</Text> :
673
720
  <Text>{l}</Text>}
674
721
  </Text>
@@ -678,53 +725,69 @@ function App() {
678
725
  case "tool":
679
726
  return (
680
727
  <Box key={msg.id}>
681
- <Text><Text color="#00FFFF"> ● </Text><Text bold color="#FF00FF">{msg.text}</Text></Text>
728
+ <Text><Text color={theme.colors.response}> ● </Text><Text bold color={theme.colors.tool}>{msg.text}</Text></Text>
682
729
  </Box>
683
730
  );
684
731
  case "tool-result":
685
- return <Text key={msg.id} color="#008B8B"> {msg.text}</Text>;
732
+ return <Text key={msg.id} color={theme.colors.toolResult}> {msg.text}</Text>;
686
733
  case "error":
687
- return <Text key={msg.id} color="red"> {msg.text}</Text>;
734
+ return <Text key={msg.id} color={theme.colors.error}> {msg.text}</Text>;
688
735
  case "info":
689
- return <Text key={msg.id} color="#008B8B"> {msg.text}</Text>;
736
+ return <Text key={msg.id} color={theme.colors.muted}> {msg.text}</Text>;
690
737
  default:
691
738
  return <Text key={msg.id}>{msg.text}</Text>;
692
739
  }
693
740
  })}
694
741
 
695
742
  {/* ═══ SPINNER ═══ */}
696
- {loading && !approval && !streaming && <NeonSpinner message={spinnerMsg} />}
697
- {streaming && !loading && <StreamingIndicator />}
743
+ {loading && !approval && !streaming && <NeonSpinner message={spinnerMsg} colors={theme.colors} />}
744
+ {streaming && !loading && <StreamingIndicator colors={theme.colors} />}
698
745
 
699
746
  {/* ═══ APPROVAL PROMPT ═══ */}
700
747
  {approval && (
701
- <Box flexDirection="column" borderStyle="single" borderColor="#FF8C00" paddingX={1} marginTop={1}>
702
- <Text bold color="#FF8C00">⚠ Approve {approval.tool}?</Text>
748
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginTop={1}>
749
+ <Text bold color={theme.colors.warning}>⚠ Approve {approval.tool}?</Text>
703
750
  {approval.tool === "write_file" && approval.args.path ? (
704
- <Text color="#008B8B">{" 📄 "}{String(approval.args.path)}</Text>
751
+ <Text color={theme.colors.muted}>{" 📄 "}{String(approval.args.path)}</Text>
705
752
  ) : null}
706
753
  {approval.tool === "write_file" && approval.args.content ? (
707
- <Text color="#008B8B">{" "}{String(approval.args.content).split("\n").length}{" lines, "}{String(approval.args.content).length}{"B"}</Text>
754
+ <Text color={theme.colors.muted}>{" "}{String(approval.args.content).split("\n").length}{" lines, "}{String(approval.args.content).length}{"B"}</Text>
708
755
  ) : null}
709
756
  {approval.tool === "run_command" && approval.args.command ? (
710
- <Text color="#008B8B">{" $ "}{String(approval.args.command)}</Text>
757
+ <Text color={theme.colors.muted}>{" $ "}{String(approval.args.command)}</Text>
711
758
  ) : null}
712
759
  <Text>
713
- <Text color="#00FF00" bold> [y]</Text><Text>es </Text>
714
- <Text color="#FF0000" bold>[n]</Text><Text>o </Text>
715
- <Text color="#00FFFF" bold>[a]</Text><Text>lways</Text>
760
+ <Text color={theme.colors.success} bold> [y]</Text><Text>es </Text>
761
+ <Text color={theme.colors.error} bold>[n]</Text><Text>o </Text>
762
+ <Text color={theme.colors.primary} bold>[a]</Text><Text>lways</Text>
716
763
  </Text>
717
764
  </Box>
718
765
  )}
719
766
 
767
+ {/* ═══ THEME PICKER ═══ */}
768
+ {themePicker && (
769
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
770
+ <Text bold color={theme.colors.secondary}>Choose a theme:</Text>
771
+ {listThemes().map((key, i) => (
772
+ <Text key={key}>
773
+ {i === themePickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
774
+ <Text color={i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{key}</Text>
775
+ <Text color={theme.colors.muted}>{" — "}{THEMES[key].description}</Text>
776
+ {key === theme.name.toLowerCase() ? <Text color={theme.colors.muted}> (current)</Text> : null}
777
+ </Text>
778
+ ))}
779
+ <Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
780
+ </Box>
781
+ )}
782
+
720
783
  {/* ═══ SESSION PICKER ═══ */}
721
784
  {sessionPicker && (
722
- <Box flexDirection="column" borderStyle="single" borderColor="#FF00FF" paddingX={1} marginBottom={0}>
723
- <Text bold color="#FF00FF">Resume a session:</Text>
785
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.secondary} paddingX={1} marginBottom={0}>
786
+ <Text bold color={theme.colors.secondary}>Resume a session:</Text>
724
787
  {sessionPicker.map((s, i) => (
725
788
  <Text key={s.id}>
726
- {i === sessionPickerIndex ? <Text color="#FF00FF" bold>{"▸ "}</Text> : <Text>{" "}</Text>}
727
- <Text color={i === sessionPickerIndex ? "#FF00FF" : "#008B8B"}>{s.display}</Text>
789
+ {i === sessionPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
790
+ <Text color={i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted}>{s.display}</Text>
728
791
  </Text>
729
792
  ))}
730
793
  <Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
@@ -733,12 +796,12 @@ function App() {
733
796
 
734
797
  {/* ═══ COMMAND SUGGESTIONS ═══ */}
735
798
  {showSuggestions && (
736
- <Box flexDirection="column" borderStyle="single" borderColor="#008B8B" paddingX={1} marginBottom={0}>
799
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.muted} paddingX={1} marginBottom={0}>
737
800
  {cmdMatches.slice(0, 6).map((c, i) => (
738
801
  <Text key={i}>
739
- {i === cmdIndex ? <Text color="#FF00FF" bold>{"▸ "}</Text> : <Text>{" "}</Text>}
740
- <Text color={i === cmdIndex ? "#FF00FF" : "#00FFFF"} bold>{c.cmd}</Text>
741
- <Text color="#008B8B">{" — "}{c.desc}</Text>
802
+ {i === cmdIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
803
+ <Text color={i === cmdIndex ? theme.colors.suggestion : theme.colors.primary} bold>{c.cmd}</Text>
804
+ <Text color={theme.colors.muted}>{" — "}{c.desc}</Text>
742
805
  </Text>
743
806
  ))}
744
807
  <Text dimColor>{" ↑↓ navigate · Tab select"}</Text>
@@ -746,14 +809,14 @@ function App() {
746
809
  )}
747
810
 
748
811
  {/* ═══ INPUT BOX (always at bottom) ═══ */}
749
- <Box borderStyle="single" borderColor={approval ? "#FF8C00" : "#00FFFF"} paddingX={1}>
750
- <Text color="#FF00FF" bold>{"> "}</Text>
812
+ <Box borderStyle="single" borderColor={approval ? theme.colors.warning : theme.colors.border} paddingX={1}>
813
+ <Text color={theme.colors.secondary} bold>{"> "}</Text>
751
814
  {approval ? (
752
- <Text color="#FF8C00">waiting for approval...</Text>
815
+ <Text color={theme.colors.warning}>waiting for approval...</Text>
753
816
  ) : ready && !loading ? (
754
817
  <Box>
755
818
  {pastedChunks.map((p) => (
756
- <Text key={p.id} color="#008B8B">[Pasted text #{p.id} +{p.lines} lines]</Text>
819
+ <Text key={p.id} color={theme.colors.muted}>[Pasted text #{p.id} +{p.lines} lines]</Text>
757
820
  ))}
758
821
  <TextInput
759
822
  key={inputKey}
package/src/themes.ts ADDED
@@ -0,0 +1,173 @@
1
+ export interface Theme {
2
+ name: string;
3
+ description: string;
4
+ colors: {
5
+ primary: string; // Main accent (borders, highlights)
6
+ secondary: string; // Secondary accent (banner, headings)
7
+ muted: string; // Dimmed text (hints, timestamps)
8
+ text: string; // Normal text
9
+ userInput: string; // User input text
10
+ response: string; // AI response marker
11
+ tool: string; // Tool call text
12
+ toolResult: string; // Tool result text
13
+ error: string; // Error messages
14
+ success: string; // Success messages
15
+ warning: string; // Warning/approval prompts
16
+ spinner: string; // Spinner color
17
+ border: string; // Border color
18
+ suggestion: string; // Highlighted suggestion
19
+ };
20
+ }
21
+
22
+ export const THEMES: Record<string, Theme> = {
23
+ neon: {
24
+ name: "Neon",
25
+ description: "Cyberpunk neon (default)",
26
+ colors: {
27
+ primary: "#00FFFF",
28
+ secondary: "#FF00FF",
29
+ muted: "#008B8B",
30
+ text: "",
31
+ userInput: "#008B8B",
32
+ response: "#00FFFF",
33
+ tool: "#FF00FF",
34
+ toolResult: "#008B8B",
35
+ error: "red",
36
+ success: "#00FF00",
37
+ warning: "#FF8C00",
38
+ spinner: "#00FFFF",
39
+ border: "#00FFFF",
40
+ suggestion: "#FF00FF",
41
+ },
42
+ },
43
+ dracula: {
44
+ name: "Dracula",
45
+ description: "Dark purple tones",
46
+ colors: {
47
+ primary: "#BD93F9",
48
+ secondary: "#FF79C6",
49
+ muted: "#6272A4",
50
+ text: "#F8F8F2",
51
+ userInput: "#8BE9FD",
52
+ response: "#BD93F9",
53
+ tool: "#FF79C6",
54
+ toolResult: "#6272A4",
55
+ error: "#FF5555",
56
+ success: "#50FA7B",
57
+ warning: "#FFB86C",
58
+ spinner: "#BD93F9",
59
+ border: "#BD93F9",
60
+ suggestion: "#FF79C6",
61
+ },
62
+ },
63
+ gruvbox: {
64
+ name: "Gruvbox",
65
+ description: "Warm retro tones",
66
+ colors: {
67
+ primary: "#FE8019",
68
+ secondary: "#FABD2F",
69
+ muted: "#928374",
70
+ text: "#EBDBB2",
71
+ userInput: "#83A598",
72
+ response: "#FE8019",
73
+ tool: "#FABD2F",
74
+ toolResult: "#928374",
75
+ error: "#FB4934",
76
+ success: "#B8BB26",
77
+ warning: "#FABD2F",
78
+ spinner: "#FE8019",
79
+ border: "#FE8019",
80
+ suggestion: "#FABD2F",
81
+ },
82
+ },
83
+ nord: {
84
+ name: "Nord",
85
+ description: "Cool arctic blues",
86
+ colors: {
87
+ primary: "#88C0D0",
88
+ secondary: "#81A1C1",
89
+ muted: "#4C566A",
90
+ text: "#ECEFF4",
91
+ userInput: "#88C0D0",
92
+ response: "#81A1C1",
93
+ tool: "#5E81AC",
94
+ toolResult: "#4C566A",
95
+ error: "#BF616A",
96
+ success: "#A3BE8C",
97
+ warning: "#EBCB8B",
98
+ spinner: "#88C0D0",
99
+ border: "#81A1C1",
100
+ suggestion: "#88C0D0",
101
+ },
102
+ },
103
+ mono: {
104
+ name: "Mono",
105
+ description: "Clean monochrome — easy on the eyes",
106
+ colors: {
107
+ primary: "#AAAAAA",
108
+ secondary: "#FFFFFF",
109
+ muted: "#666666",
110
+ text: "#CCCCCC",
111
+ userInput: "#AAAAAA",
112
+ response: "#FFFFFF",
113
+ tool: "#CCCCCC",
114
+ toolResult: "#666666",
115
+ error: "#FF6666",
116
+ success: "#66FF66",
117
+ warning: "#FFAA66",
118
+ spinner: "#AAAAAA",
119
+ border: "#888888",
120
+ suggestion: "#FFFFFF",
121
+ },
122
+ },
123
+ solarized: {
124
+ name: "Solarized",
125
+ description: "Solarized dark",
126
+ colors: {
127
+ primary: "#268BD2",
128
+ secondary: "#2AA198",
129
+ muted: "#586E75",
130
+ text: "#839496",
131
+ userInput: "#2AA198",
132
+ response: "#268BD2",
133
+ tool: "#B58900",
134
+ toolResult: "#586E75",
135
+ error: "#DC322F",
136
+ success: "#859900",
137
+ warning: "#CB4B16",
138
+ spinner: "#268BD2",
139
+ border: "#268BD2",
140
+ suggestion: "#2AA198",
141
+ },
142
+ },
143
+ hacker: {
144
+ name: "Hacker",
145
+ description: "Green on black — classic terminal",
146
+ colors: {
147
+ primary: "#00FF00",
148
+ secondary: "#00CC00",
149
+ muted: "#006600",
150
+ text: "#00DD00",
151
+ userInput: "#00FF00",
152
+ response: "#00FF00",
153
+ tool: "#00CC00",
154
+ toolResult: "#006600",
155
+ error: "#FF0000",
156
+ success: "#00FF00",
157
+ warning: "#FFFF00",
158
+ spinner: "#00FF00",
159
+ border: "#00FF00",
160
+ suggestion: "#00CC00",
161
+ },
162
+ },
163
+ };
164
+
165
+ export const DEFAULT_THEME = "neon";
166
+
167
+ export function getTheme(name: string): Theme {
168
+ return THEMES[name] ?? THEMES[DEFAULT_THEME];
169
+ }
170
+
171
+ export function listThemes(): string[] {
172
+ return Object.keys(THEMES);
173
+ }