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 +4 -0
- package/dist/index.js +59 -13
- package/dist/themes.d.ts +24 -0
- package/dist/themes.js +149 -0
- package/package.json +1 -1
- package/src/index.tsx +108 -45
- package/src/themes.ts +173 -0
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
585
|
-
l.startsWith("# ") || l.startsWith("## ") ? _jsx(Text, { bold: true, color:
|
|
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:
|
|
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:
|
|
637
|
+
return _jsxs(Text, { color: theme.colors.toolResult, children: [" ", msg.text] }, msg.id);
|
|
592
638
|
case "error":
|
|
593
|
-
return _jsxs(Text, { color:
|
|
639
|
+
return _jsxs(Text, { color: theme.colors.error, children: [" ", msg.text] }, msg.id);
|
|
594
640
|
case "info":
|
|
595
|
-
return _jsxs(Text, { color:
|
|
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:
|
|
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}` : ""] }) }))] }));
|
package/dist/themes.d.ts
ADDED
|
@@ -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
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=
|
|
73
|
-
{" "}<Text bold color=
|
|
74
|
-
{" "}<Text color=
|
|
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=
|
|
94
|
-
{" "}<Text color=
|
|
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=
|
|
678
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.border} paddingX={1}>
|
|
632
679
|
{codeLines.map((line, i) => (
|
|
633
|
-
<Text key={`c${i}`} color=
|
|
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={
|
|
683
|
+
<Text key={`m${i}`} color={theme.colors.secondary}>{line}</Text>
|
|
637
684
|
))}
|
|
638
685
|
<Text>
|
|
639
|
-
<Text color=
|
|
640
|
-
{" "}<Text color=
|
|
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=
|
|
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=
|
|
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("✔") ?
|
|
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=
|
|
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=
|
|
670
|
-
{l.startsWith("```") ? <Text color=
|
|
671
|
-
l.startsWith("# ") || l.startsWith("## ") ? <Text bold color=
|
|
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=
|
|
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=
|
|
732
|
+
return <Text key={msg.id} color={theme.colors.toolResult}> {msg.text}</Text>;
|
|
686
733
|
case "error":
|
|
687
|
-
return <Text key={msg.id} color=
|
|
734
|
+
return <Text key={msg.id} color={theme.colors.error}> {msg.text}</Text>;
|
|
688
735
|
case "info":
|
|
689
|
-
return <Text key={msg.id} color=
|
|
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=
|
|
702
|
-
<Text bold color=
|
|
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=
|
|
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=
|
|
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=
|
|
757
|
+
<Text color={theme.colors.muted}>{" $ "}{String(approval.args.command)}</Text>
|
|
711
758
|
) : null}
|
|
712
759
|
<Text>
|
|
713
|
-
<Text color=
|
|
714
|
-
<Text color=
|
|
715
|
-
<Text color=
|
|
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=
|
|
723
|
-
<Text bold color=
|
|
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=
|
|
727
|
-
<Text color={i === sessionPickerIndex ?
|
|
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=
|
|
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=
|
|
740
|
-
<Text color={i === cmdIndex ?
|
|
741
|
-
<Text color=
|
|
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 ?
|
|
750
|
-
<Text color=
|
|
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=
|
|
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=
|
|
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
|
+
}
|