snow-ai 0.4.8 → 0.4.10

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.
Files changed (48) hide show
  1. package/dist/app.js +30 -12
  2. package/dist/cli.js +6 -0
  3. package/dist/i18n/lang/en.js +21 -0
  4. package/dist/i18n/lang/es.js +21 -0
  5. package/dist/i18n/lang/ja.js +21 -0
  6. package/dist/i18n/lang/ko.js +21 -0
  7. package/dist/i18n/lang/zh-TW.js +21 -0
  8. package/dist/i18n/lang/zh.js +21 -0
  9. package/dist/i18n/types.d.ts +21 -0
  10. package/dist/mcp/todo.js +1 -1
  11. package/dist/ui/components/AgentPickerPanel.js +8 -6
  12. package/dist/ui/components/ChatInput.js +23 -21
  13. package/dist/ui/components/CommandPanel.js +7 -5
  14. package/dist/ui/components/DiffViewer.js +6 -4
  15. package/dist/ui/components/FileList.js +8 -6
  16. package/dist/ui/components/Menu.d.ts +1 -1
  17. package/dist/ui/components/Menu.js +8 -6
  18. package/dist/ui/components/PendingMessages.js +7 -5
  19. package/dist/ui/components/TodoPickerPanel.js +12 -10
  20. package/dist/ui/components/TodoTree.js +7 -5
  21. package/dist/ui/components/ToolConfirmation.js +14 -12
  22. package/dist/ui/components/ToolResultPreview.js +17 -3
  23. package/dist/ui/contexts/ThemeContext.d.ts +13 -0
  24. package/dist/ui/contexts/ThemeContext.js +28 -0
  25. package/dist/ui/pages/ChatScreen.js +21 -19
  26. package/dist/ui/pages/CodeBaseConfigScreen.js +30 -28
  27. package/dist/ui/pages/ConfigScreen.js +76 -74
  28. package/dist/ui/pages/CustomHeadersScreen.js +33 -31
  29. package/dist/ui/pages/LanguageSettingsScreen.js +6 -4
  30. package/dist/ui/pages/ProxyConfigScreen.js +15 -13
  31. package/dist/ui/pages/SensitiveCommandConfigScreen.js +12 -10
  32. package/dist/ui/pages/SubAgentConfigScreen.js +12 -10
  33. package/dist/ui/pages/SubAgentListScreen.js +11 -9
  34. package/dist/ui/pages/SystemPromptConfigScreen.js +21 -19
  35. package/dist/ui/pages/ThemeSettingsScreen.d.ts +7 -0
  36. package/dist/ui/pages/ThemeSettingsScreen.js +106 -0
  37. package/dist/ui/pages/WelcomeScreen.js +11 -1
  38. package/dist/ui/themes/index.d.ts +23 -0
  39. package/dist/ui/themes/index.js +140 -0
  40. package/dist/utils/configManager.js +11 -3
  41. package/dist/utils/logger.d.ts +9 -3
  42. package/dist/utils/logger.js +28 -3
  43. package/dist/utils/mcpToolsManager.d.ts +1 -1
  44. package/dist/utils/mcpToolsManager.js +13 -9
  45. package/dist/utils/themeConfig.d.ts +21 -0
  46. package/dist/utils/themeConfig.js +61 -0
  47. package/dist/utils/toolExecutor.js +11 -1
  48. package/package.json +3 -2
@@ -3,7 +3,9 @@ import { Box, Text } from 'ink';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { useTerminalSize } from '../../hooks/useTerminalSize.js';
6
+ import { useTheme } from '../contexts/ThemeContext.js';
6
7
  const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10, rootPath = process.cwd(), onFilteredCountChange, searchMode = 'file', }, ref) => {
8
+ const { theme } = useTheme();
7
9
  const [files, setFiles] = useState([]);
8
10
  const [isLoading, setIsLoading] = useState(false);
9
11
  // Get terminal size for dynamic content display
@@ -325,7 +327,7 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
325
327
  }
326
328
  if (filteredFiles.length === 0) {
327
329
  return (React.createElement(Box, { paddingX: 1, marginTop: 1 },
328
- React.createElement(Text, { color: "gray", dimColor: true }, "No files found")));
330
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, "No files found")));
329
331
  }
330
332
  return (React.createElement(Box, { paddingX: 1, marginTop: 1, flexDirection: "column" },
331
333
  React.createElement(Box, { marginBottom: 1 },
@@ -335,20 +337,20 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
335
337
  allFilteredFiles.length > effectiveMaxItems &&
336
338
  `(${selectedIndex + 1}/${allFilteredFiles.length})`)),
337
339
  filteredFiles.map((file, index) => (React.createElement(Box, { key: `${file.path}-${file.lineNumber || 0}`, flexDirection: "column" },
338
- React.createElement(Text, { backgroundColor: index === displaySelectedIndex ? '#1E3A8A' : undefined, color: index === displaySelectedIndex
339
- ? '#FFFFFF'
340
+ React.createElement(Text, { backgroundColor: index === displaySelectedIndex ? theme.colors.menuSelected : undefined, color: index === displaySelectedIndex
341
+ ? theme.colors.menuNormal
340
342
  : file.isDirectory
341
- ? 'yellow'
343
+ ? theme.colors.warning
342
344
  : 'white' }, searchMode === 'content' && file.lineNumber !== undefined
343
345
  ? `${file.path}:${file.lineNumber}`
344
346
  : file.isDirectory
345
347
  ? '◇ ' + file.path
346
348
  : '◆ ' + file.path),
347
- searchMode === 'content' && file.lineContent && (React.createElement(Text, { backgroundColor: index === displaySelectedIndex ? '#1E3A8A' : undefined, color: index === displaySelectedIndex ? '#D1D5DB' : 'gray', dimColor: true },
349
+ searchMode === 'content' && file.lineContent && (React.createElement(Text, { backgroundColor: index === displaySelectedIndex ? theme.colors.menuSelected : undefined, color: index === displaySelectedIndex ? theme.colors.menuSecondary : theme.colors.menuSecondary, dimColor: true },
348
350
  ' ',
349
351
  file.lineContent))))),
350
352
  allFilteredFiles.length > effectiveMaxItems && (React.createElement(Box, { marginTop: 1 },
351
- React.createElement(Text, { color: "gray", dimColor: true },
353
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
352
354
  "\u2191\u2193 to scroll \u00B7 ",
353
355
  allFilteredFiles.length - effectiveMaxItems,
354
356
  ' ',
@@ -9,7 +9,7 @@ type MenuOption = {
9
9
  type Props = {
10
10
  options: MenuOption[];
11
11
  onSelect: (value: string) => void;
12
- onSelectionChange?: (infoText: string) => void;
12
+ onSelectionChange?: (infoText: string, value: string) => void;
13
13
  maxHeight?: number;
14
14
  };
15
15
  declare function Menu({ options, onSelect, onSelectionChange, maxHeight }: Props): React.JSX.Element;
@@ -2,11 +2,13 @@ import React, { useState, useCallback } from 'react';
2
2
  import { Box, Text, useInput, useStdout } from 'ink';
3
3
  import { resetTerminal } from '../../utils/terminal.js';
4
4
  import { useI18n } from '../../i18n/index.js';
5
+ import { useTheme } from '../contexts/ThemeContext.js';
5
6
  function Menu({ options, onSelect, onSelectionChange, maxHeight }) {
6
7
  const [selectedIndex, setSelectedIndex] = useState(0);
7
8
  const [scrollOffset, setScrollOffset] = useState(0);
8
9
  const { stdout } = useStdout();
9
10
  const { t } = useI18n();
11
+ const { theme } = useTheme();
10
12
  // Calculate available height
11
13
  const terminalHeight = stdout?.rows || 24;
12
14
  const headerHeight = 8; // Space for header, borders, etc.
@@ -15,7 +17,7 @@ function Menu({ options, onSelect, onSelectionChange, maxHeight }) {
15
17
  React.useEffect(() => {
16
18
  const currentOption = options[selectedIndex];
17
19
  if (onSelectionChange && currentOption?.infoText) {
18
- onSelectionChange(currentOption.infoText);
20
+ onSelectionChange(currentOption.infoText, currentOption.value);
19
21
  }
20
22
  }, [selectedIndex, options, onSelectionChange]);
21
23
  // Auto-scroll to keep selected item visible
@@ -56,9 +58,9 @@ function Menu({ options, onSelect, onSelectionChange, maxHeight }) {
56
58
  const moreBelowCount = options.length - (scrollOffset + visibleItemCount);
57
59
  return (React.createElement(Box, { flexDirection: "column", width: '100%', padding: 1 },
58
60
  React.createElement(Box, { marginBottom: 1 },
59
- React.createElement(Text, { color: "cyan" }, t.menu.navigate)),
61
+ React.createElement(Text, { color: theme.colors.menuInfo }, t.menu.navigate)),
60
62
  hasMoreAbove && (React.createElement(Box, null,
61
- React.createElement(Text, { color: "gray", dimColor: true },
63
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
62
64
  "\u2191 +",
63
65
  moreAboveCount,
64
66
  " more above"))),
@@ -66,13 +68,13 @@ function Menu({ options, onSelect, onSelectionChange, maxHeight }) {
66
68
  const actualIndex = scrollOffset + index;
67
69
  return (React.createElement(Box, { key: option.value },
68
70
  React.createElement(Text, { color: actualIndex === selectedIndex
69
- ? 'green'
70
- : option.color || 'white', bold: true },
71
+ ? theme.colors.menuSelected
72
+ : option.color || theme.colors.menuNormal, bold: true },
71
73
  actualIndex === selectedIndex ? '❯ ' : ' ',
72
74
  option.label)));
73
75
  }),
74
76
  hasMoreBelow && (React.createElement(Box, null,
75
- React.createElement(Text, { color: "gray", dimColor: true },
77
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
76
78
  "\u2193 +",
77
79
  moreBelowCount,
78
80
  " more below")))));
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import { useTheme } from '../contexts/ThemeContext.js';
3
4
  export default function PendingMessages({ pendingMessages }) {
5
+ const { theme } = useTheme();
4
6
  if (pendingMessages.length === 0) {
5
7
  return null;
6
8
  }
7
- return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1 },
8
- React.createElement(Text, { color: "yellow", bold: true },
9
+ return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.warning, paddingX: 1 },
10
+ React.createElement(Text, { color: theme.colors.warning, bold: true },
9
11
  "\u2B11 Pending Messages (",
10
12
  pendingMessages.length,
11
13
  ")"),
@@ -15,13 +17,13 @@ export default function PendingMessages({ pendingMessages }) {
15
17
  index + 1,
16
18
  "."),
17
19
  React.createElement(Box, { marginLeft: 1 },
18
- React.createElement(Text, { color: "gray" }, message.text.length > 60 ? `${message.text.substring(0, 60)}...` : message.text))),
20
+ React.createElement(Text, { color: theme.colors.menuSecondary }, message.text.length > 60 ? `${message.text.substring(0, 60)}...` : message.text))),
19
21
  message.images && message.images.length > 0 && (React.createElement(Box, { marginLeft: 3 },
20
- React.createElement(Text, { color: "gray", dimColor: true },
22
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
21
23
  "\u2514\u2500 ",
22
24
  message.images.length,
23
25
  " image",
24
26
  message.images.length > 1 ? 's' : '',
25
27
  " attached")))))),
26
- React.createElement(Text, { color: "yellow", dimColor: true }, "Will be sent after tool execution completes")));
28
+ React.createElement(Text, { color: theme.colors.warning, dimColor: true }, "Will be sent after tool execution completes")));
27
29
  }
@@ -1,7 +1,9 @@
1
1
  import React, { memo, useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { Alert } from '@inkjs/ui';
4
+ import { useTheme } from '../contexts/ThemeContext.js';
4
5
  const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, maxHeight, isLoading = false, searchQuery = '', totalCount = 0, }) => {
6
+ const { theme } = useTheme();
5
7
  // Fixed maximum display items to prevent rendering issues
6
8
  const MAX_DISPLAY_ITEMS = 5;
7
9
  const effectiveMaxItems = maxHeight
@@ -39,7 +41,7 @@ const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, ma
39
41
  React.createElement(Box, { width: "100%" },
40
42
  React.createElement(Box, { flexDirection: "column", width: "100%" },
41
43
  React.createElement(Box, null,
42
- React.createElement(Text, { color: "yellow", bold: true }, "TODO Selection")),
44
+ React.createElement(Text, { color: theme.colors.warning, bold: true }, "TODO Selection")),
43
45
  React.createElement(Box, { marginTop: 1 },
44
46
  React.createElement(Alert, { variant: "info" }, "Scanning project for TODO comments..."))))));
45
47
  }
@@ -49,7 +51,7 @@ const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, ma
49
51
  React.createElement(Box, { width: "100%" },
50
52
  React.createElement(Box, { flexDirection: "column", width: "100%" },
51
53
  React.createElement(Box, null,
52
- React.createElement(Text, { color: "yellow", bold: true }, "TODO Selection")),
54
+ React.createElement(Text, { color: theme.colors.warning, bold: true }, "TODO Selection")),
53
55
  React.createElement(Box, { marginTop: 1 },
54
56
  React.createElement(Alert, { variant: "info" }, "No TODO comments found in the project"))))));
55
57
  }
@@ -59,7 +61,7 @@ const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, ma
59
61
  React.createElement(Box, { width: "100%" },
60
62
  React.createElement(Box, { flexDirection: "column", width: "100%" },
61
63
  React.createElement(Box, null,
62
- React.createElement(Text, { color: "yellow", bold: true }, "TODO Selection")),
64
+ React.createElement(Text, { color: theme.colors.warning, bold: true }, "TODO Selection")),
63
65
  React.createElement(Box, { marginTop: 1 },
64
66
  React.createElement(Alert, { variant: "warning" },
65
67
  "No TODOs match \"",
@@ -68,13 +70,13 @@ const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, ma
68
70
  totalCount,
69
71
  ")")),
70
72
  React.createElement(Box, { marginTop: 1 },
71
- React.createElement(Text, { color: "gray", dimColor: true }, "Type to filter \u00B7 Backspace to clear search"))))));
73
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, "Type to filter \u00B7 Backspace to clear search"))))));
72
74
  }
73
75
  return (React.createElement(Box, { flexDirection: "column" },
74
76
  React.createElement(Box, { width: "100%" },
75
77
  React.createElement(Box, { flexDirection: "column", width: "100%" },
76
78
  React.createElement(Box, null,
77
- React.createElement(Text, { color: "yellow", bold: true },
79
+ React.createElement(Text, { color: theme.colors.warning, bold: true },
78
80
  "Select TODOs",
79
81
  ' ',
80
82
  todos.length > effectiveMaxItems &&
@@ -84,14 +86,14 @@ const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, ma
84
86
  totalCount > todos.length &&
85
87
  ` (${todos.length}/${totalCount})`)),
86
88
  React.createElement(Box, { marginTop: 1 },
87
- React.createElement(Text, { color: "gray", dimColor: true }, searchQuery
89
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, searchQuery
88
90
  ? 'Type to filter · Backspace to clear · Space: toggle · Enter: confirm'
89
91
  : 'Type to search · Space: toggle · Enter: confirm · Esc: cancel')),
90
92
  displayedTodos.map((todo, index) => {
91
93
  const isSelected = index === displayedSelectedIndex;
92
94
  const isChecked = selectedTodos.has(todo.id);
93
95
  return (React.createElement(Box, { key: todo.id, flexDirection: "column", width: "100%" },
94
- React.createElement(Text, { color: isSelected ? 'green' : 'gray', bold: true },
96
+ React.createElement(Text, { color: isSelected ? theme.colors.success : theme.colors.menuSecondary, bold: true },
95
97
  isSelected ? '❯ ' : ' ',
96
98
  isChecked ? '[✓]' : '[ ]',
97
99
  " ",
@@ -99,17 +101,17 @@ const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, ma
99
101
  ":",
100
102
  todo.line),
101
103
  React.createElement(Box, { marginLeft: 5 },
102
- React.createElement(Text, { color: isSelected ? 'green' : 'gray', dimColor: !isSelected },
104
+ React.createElement(Text, { color: isSelected ? theme.colors.success : theme.colors.menuSecondary, dimColor: !isSelected },
103
105
  "\u2514\u2500 ",
104
106
  todo.content))));
105
107
  }),
106
108
  todos.length > effectiveMaxItems && (React.createElement(Box, { marginTop: 1 },
107
- React.createElement(Text, { color: "gray", dimColor: true },
109
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
108
110
  "\u2191\u2193 to scroll \u00B7 ",
109
111
  todos.length - effectiveMaxItems,
110
112
  " more hidden"))),
111
113
  selectedTodos.size > 0 && (React.createElement(Box, { marginTop: 1 },
112
- React.createElement(Text, { color: "cyan" },
114
+ React.createElement(Text, { color: theme.colors.menuInfo },
113
115
  selectedTodos.size,
114
116
  " TODO(s) selected")))))));
115
117
  });
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import { useTheme } from '../contexts/ThemeContext.js';
3
4
  /**
4
5
  * TODO Tree 组件 - 显示带复选框的任务树
5
6
  */
6
7
  export default function TodoTree({ todos }) {
8
+ const { theme } = useTheme();
7
9
  if (todos.length === 0) {
8
10
  return null;
9
11
  }
@@ -30,9 +32,9 @@ export default function TodoTree({ todos }) {
30
32
  const getStatusColor = (status) => {
31
33
  switch (status) {
32
34
  case 'completed':
33
- return 'green';
35
+ return theme.colors.success;
34
36
  case 'pending':
35
- return 'gray';
37
+ return theme.colors.menuSecondary;
36
38
  }
37
39
  };
38
40
  const renderTodo = (todo, depth = 0) => {
@@ -49,10 +51,10 @@ export default function TodoTree({ todos }) {
49
51
  todo.content)),
50
52
  children.map(child => renderTodo(child, depth + 1))));
51
53
  };
52
- return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1 },
54
+ return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.menuInfo, paddingX: 1, marginBottom: 1 },
53
55
  React.createElement(Box, { marginBottom: 0 },
54
- React.createElement(Text, { bold: true, color: "cyan" }, "TODO List")),
56
+ React.createElement(Text, { bold: true, color: theme.colors.menuInfo }, "TODO List")),
55
57
  rootTodos.map(todo => renderTodo(todo)),
56
58
  React.createElement(Box, { marginTop: 0 },
57
- React.createElement(Text, { dimColor: true, color: "gray" }, "[ ] Pending \u00B7 [\u2713] Completed"))));
59
+ React.createElement(Text, { dimColor: true, color: theme.colors.menuSecondary }, "[ ] Pending \u00B7 [\u2713] Completed"))));
58
60
  }
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import SelectInput from 'ink-select-input';
5
5
  import { isSensitiveCommand } from '../../utils/sensitiveCommandManager.js';
6
+ import { useTheme } from '../contexts/ThemeContext.js';
6
7
  // Helper function to format argument values with truncation
7
8
  function formatArgumentValue(value, maxLength = 100) {
8
9
  if (value === null || value === undefined) {
@@ -41,6 +42,7 @@ function formatArgumentsAsTree(args, toolName) {
41
42
  }));
42
43
  }
43
44
  export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm, }) {
45
+ const { theme } = useTheme();
44
46
  const [hasSelected, setHasSelected] = useState(false);
45
47
  const [showRejectInput, setShowRejectInput] = useState(false);
46
48
  const [rejectReason, setRejectReason] = useState('');
@@ -135,18 +137,18 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
135
137
  onConfirm({ type: 'reject_with_reply', reason: rejectReason.trim() });
136
138
  }
137
139
  };
138
- return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1, borderStyle: 'round', borderColor: 'yellow', paddingX: 1 },
140
+ return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1, borderStyle: 'round', borderColor: theme.colors.warning, paddingX: 1 },
139
141
  React.createElement(Box, { marginBottom: 1 },
140
- React.createElement(Text, { bold: true, color: "yellow" }, "[Tool Confirmation]")),
142
+ React.createElement(Text, { bold: true, color: theme.colors.warning }, "[Tool Confirmation]")),
141
143
  !formattedAllTools && (React.createElement(React.Fragment, null,
142
144
  React.createElement(Box, { marginBottom: 1 },
143
145
  React.createElement(Text, null,
144
146
  "Tool:",
145
147
  ' ',
146
- React.createElement(Text, { bold: true, color: "cyan" }, toolName))),
148
+ React.createElement(Text, { bold: true, color: theme.colors.menuInfo }, toolName))),
147
149
  sensitiveCommandCheck.isSensitive && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
148
150
  React.createElement(Box, { marginBottom: 1 },
149
- React.createElement(Text, { bold: true, color: "red" }, "SENSITIVE COMMAND DETECTED")),
151
+ React.createElement(Text, { bold: true, color: theme.colors.error }, "SENSITIVE COMMAND DETECTED")),
150
152
  React.createElement(Box, { flexDirection: "column", gap: 0 },
151
153
  React.createElement(Box, null,
152
154
  React.createElement(Text, { dimColor: true }, "Pattern: "),
@@ -155,11 +157,11 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
155
157
  React.createElement(Text, { dimColor: true }, "Reason: "),
156
158
  React.createElement(Text, { color: "white" }, sensitiveCommandCheck.matchedCommand?.description))),
157
159
  React.createElement(Box, { marginTop: 1, paddingX: 1, paddingY: 0 },
158
- React.createElement(Text, { color: "yellow", italic: true }, "This command requires confirmation even in YOLO/Always-Approved mode")))),
160
+ React.createElement(Text, { color: theme.colors.warning, italic: true }, "This command requires confirmation even in YOLO/Always-Approved mode")))),
159
161
  formattedArgs && formattedArgs.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
160
162
  React.createElement(Text, { dimColor: true }, "Arguments:"),
161
163
  formattedArgs.map((arg, index) => (React.createElement(Box, { key: index, flexDirection: "column" },
162
- React.createElement(Text, { color: "gray", dimColor: true },
164
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
163
165
  arg.isLast ? '└─' : '├─',
164
166
  " ",
165
167
  arg.key,
@@ -171,15 +173,15 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
171
173
  React.createElement(Text, null,
172
174
  "Tools:",
173
175
  ' ',
174
- React.createElement(Text, { bold: true, color: "cyan" },
176
+ React.createElement(Text, { bold: true, color: theme.colors.menuInfo },
175
177
  formattedAllTools.length,
176
178
  " tools in parallel"))),
177
179
  formattedAllTools.map((tool, toolIndex) => (React.createElement(Box, { key: toolIndex, flexDirection: "column", marginBottom: toolIndex < formattedAllTools.length - 1 ? 1 : 0 },
178
- React.createElement(Text, { color: "cyan", bold: true },
180
+ React.createElement(Text, { color: theme.colors.menuInfo, bold: true },
179
181
  toolIndex + 1,
180
182
  ". ",
181
183
  tool.name),
182
- tool.args.length > 0 && (React.createElement(Box, { flexDirection: "column", paddingLeft: 2 }, tool.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: "gray", dimColor: true },
184
+ tool.args.length > 0 && (React.createElement(Box, { flexDirection: "column", paddingLeft: 2 }, tool.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: theme.colors.menuSecondary, dimColor: true },
183
185
  arg.isLast ? '└─' : '├─',
184
186
  " ",
185
187
  arg.key,
@@ -191,12 +193,12 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
191
193
  !hasSelected && !showRejectInput && (React.createElement(SelectInput, { items: items, onSelect: handleSelect })),
192
194
  showRejectInput && !hasSelected && (React.createElement(Box, { flexDirection: "column" },
193
195
  React.createElement(Box, { marginBottom: 1 },
194
- React.createElement(Text, { color: "yellow" }, "Enter rejection reason:")),
196
+ React.createElement(Text, { color: theme.colors.warning }, "Enter rejection reason:")),
195
197
  React.createElement(Box, { marginBottom: 1 },
196
- React.createElement(Text, { color: "cyan" }, "> "),
198
+ React.createElement(Text, { color: theme.colors.menuInfo }, "> "),
197
199
  React.createElement(TextInput, { value: rejectReason, onChange: setRejectReason, onSubmit: handleRejectReasonSubmit })),
198
200
  React.createElement(Box, null,
199
201
  React.createElement(Text, { dimColor: true }, "Press Enter to submit")))),
200
202
  hasSelected && (React.createElement(Box, null,
201
- React.createElement(Text, { color: "green" }, "Confirmed")))));
203
+ React.createElement(Text, { color: theme.colors.success }, "Confirmed")))));
202
204
  }
@@ -305,14 +305,28 @@ function renderTodoPreview(_toolName, data, _maxLines) {
305
305
  let todoData = data;
306
306
  // If data has content array (MCP format), extract the text
307
307
  if (data.content && Array.isArray(data.content) && data.content[0]?.text) {
308
+ const textContent = data.content[0].text;
309
+ // Skip parsing if it's a plain message string
310
+ if (textContent === 'No TODO list found' || textContent === 'TODO item not found') {
311
+ return (React.createElement(Box, { marginLeft: 2 },
312
+ React.createElement(Text, { color: "gray", dimColor: true },
313
+ "\u2514\u2500 ",
314
+ textContent)));
315
+ }
316
+ // Try to parse JSON
308
317
  try {
309
- todoData = JSON.parse(data.content[0].text);
318
+ todoData = JSON.parse(textContent);
310
319
  }
311
320
  catch (e) {
312
- // If parsing fails, just use original data
321
+ // If parsing fails, show the raw text
322
+ return (React.createElement(Box, { marginLeft: 2 },
323
+ React.createElement(Text, { color: "gray", dimColor: true },
324
+ "\u2514\u2500 ",
325
+ textContent)));
313
326
  }
314
327
  }
315
- if (!todoData.todos) {
328
+ // Check if we have valid todo data
329
+ if (!todoData.todos || !Array.isArray(todoData.todos)) {
316
330
  return (React.createElement(Box, { marginLeft: 2 },
317
331
  React.createElement(Text, { color: "gray", dimColor: true },
318
332
  "\u2514\u2500 ",
@@ -0,0 +1,13 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { ThemeType, Theme } from '../themes/index.js';
3
+ interface ThemeContextType {
4
+ theme: Theme;
5
+ themeType: ThemeType;
6
+ setThemeType: (type: ThemeType) => void;
7
+ }
8
+ interface ThemeProviderProps {
9
+ children: ReactNode;
10
+ }
11
+ export declare function ThemeProvider({ children }: ThemeProviderProps): React.JSX.Element;
12
+ export declare function useTheme(): ThemeContextType;
13
+ export {};
@@ -0,0 +1,28 @@
1
+ import React, { createContext, useContext, useState, } from 'react';
2
+ import { themes } from '../themes/index.js';
3
+ import { getCurrentTheme, setCurrentTheme, } from '../../utils/themeConfig.js';
4
+ const ThemeContext = createContext(undefined);
5
+ export function ThemeProvider({ children }) {
6
+ const [themeType, setThemeTypeState] = useState(() => {
7
+ // Load initial theme from config
8
+ return getCurrentTheme();
9
+ });
10
+ const setThemeType = (type) => {
11
+ setThemeTypeState(type);
12
+ // Persist to config file
13
+ setCurrentTheme(type);
14
+ };
15
+ const value = {
16
+ theme: themes[themeType],
17
+ themeType,
18
+ setThemeType,
19
+ };
20
+ return (React.createElement(ThemeContext.Provider, { value: value }, children));
21
+ }
22
+ export function useTheme() {
23
+ const context = useContext(ThemeContext);
24
+ if (!context) {
25
+ throw new Error('useTheme must be used within a ThemeProvider');
26
+ }
27
+ return context;
28
+ }
@@ -4,6 +4,7 @@ import Spinner from 'ink-spinner';
4
4
  import Gradient from 'ink-gradient';
5
5
  import ansiEscapes from 'ansi-escapes';
6
6
  import { useI18n } from '../../i18n/I18nContext.js';
7
+ import { useTheme } from '../contexts/ThemeContext.js';
7
8
  import ChatInput from '../components/ChatInput.js';
8
9
  import PendingMessages from '../components/PendingMessages.js';
9
10
  import MCPInfoScreen from '../components/MCPInfoScreen.js';
@@ -56,6 +57,7 @@ import '../../utils/commands/todoPicker.js';
56
57
  import '../../utils/commands/help.js';
57
58
  export default function ChatScreen({ skipWelcome }) {
58
59
  const { t } = useI18n();
60
+ const { theme } = useTheme();
59
61
  const [messages, setMessages] = useState([]);
60
62
  const [isSaving] = useState(false);
61
63
  const [pendingMessages, setPendingMessages] = useState([]);
@@ -1112,7 +1114,7 @@ export default function ChatScreen({ skipWelcome }) {
1112
1114
  .replace('{current}', terminalHeight.toString())
1113
1115
  .replace('{required}', MIN_TERMINAL_HEIGHT.toString()))),
1114
1116
  React.createElement(Box, { marginTop: 1 },
1115
- React.createElement(Text, { color: "gray", dimColor: true }, t.chatScreen.terminalMinHeight))));
1117
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.terminalMinHeight))));
1116
1118
  }
1117
1119
  return (React.createElement(Box, { flexDirection: "column", height: "100%", width: terminalWidth },
1118
1120
  React.createElement(Static, { key: remountKey, items: [
@@ -1136,7 +1138,7 @@ export default function ChatScreen({ skipWelcome }) {
1136
1138
  const pasteKey = process.platform === 'darwin' ? 'Ctrl+V' : 'Alt+V';
1137
1139
  return `• ${t.chatScreen.headerShortcuts.replace('{pasteKey}', pasteKey)}`;
1138
1140
  })()),
1139
- React.createElement(Text, { color: "gray", dimColor: true },
1141
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
1140
1142
  "\u2022",
1141
1143
  ' ',
1142
1144
  t.chatScreen.headerWorkingDirectory.replace('{directory}', workingDirectory))))),
@@ -1199,12 +1201,12 @@ export default function ChatScreen({ skipWelcome }) {
1199
1201
  }
1200
1202
  return (React.createElement(Box, { key: `msg-${index}`, marginTop: index > 0 && !shouldShowParallelIndicator ? 1 : 0, marginBottom: isLastMessage ? 1 : 0, paddingX: 1, flexDirection: "column", width: terminalWidth },
1201
1203
  isFirstInGroup && (React.createElement(Box, { marginBottom: 0 },
1202
- React.createElement(Text, { color: "#FF6EBF", dimColor: true }, "\u250C\u2500 Parallel execution"))),
1204
+ React.createElement(Text, { color: theme.colors.menuInfo, dimColor: true }, "\u250C\u2500 Parallel execution"))),
1203
1205
  React.createElement(Box, null,
1204
1206
  React.createElement(Text, { color: message.role === 'user'
1205
1207
  ? 'green'
1206
1208
  : message.role === 'command'
1207
- ? 'gray'
1209
+ ? theme.colors.menuSecondary
1208
1210
  : toolStatusColor, bold: true },
1209
1211
  shouldShowParallelIndicator && !isFirstInGroup
1210
1212
  ? '│'
@@ -1215,7 +1217,7 @@ export default function ChatScreen({ skipWelcome }) {
1215
1217
  ? '⌘'
1216
1218
  : '❆'),
1217
1219
  React.createElement(Box, { marginLeft: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(React.Fragment, null,
1218
- React.createElement(Text, { color: "gray", dimColor: true },
1220
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
1219
1221
  "\u2514\u2500 ",
1220
1222
  message.commandName),
1221
1223
  message.content && (React.createElement(Text, { color: "white" }, message.content)))) : (React.createElement(React.Fragment, null,
@@ -1225,7 +1227,7 @@ export default function ChatScreen({ skipWelcome }) {
1225
1227
  ? 'yellow'
1226
1228
  : message.content.startsWith('✓')
1227
1229
  ? 'green'
1228
- : 'red', backgroundColor: message.role === 'user' ? '#4a4a4a' : undefined }, message.content || ' ')) : (React.createElement(MarkdownRenderer, { content: message.content || ' ' })),
1230
+ : 'red', backgroundColor: message.role === 'user' ? theme.colors.border : undefined }, message.content || ' ')) : (React.createElement(MarkdownRenderer, { content: message.content || ' ' })),
1229
1231
  message.subAgentUsage &&
1230
1232
  (() => {
1231
1233
  const formatTokens = (num) => {
@@ -1233,7 +1235,7 @@ export default function ChatScreen({ skipWelcome }) {
1233
1235
  return `${(num / 1000).toFixed(1)}K`;
1234
1236
  return num.toString();
1235
1237
  };
1236
- return (React.createElement(Text, { color: "gray", dimColor: true },
1238
+ return (React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
1237
1239
  "\u2514\u2500 Usage: In=",
1238
1240
  formatTokens(message.subAgentUsage.inputTokens),
1239
1241
  ", Out=",
@@ -1251,7 +1253,7 @@ export default function ChatScreen({ skipWelcome }) {
1251
1253
  message.toolDisplay &&
1252
1254
  message.toolDisplay.args.length > 0 &&
1253
1255
  // Hide tool arguments for sub-agent internal tools
1254
- !message.subAgentInternal && (React.createElement(Box, { flexDirection: "column" }, message.toolDisplay.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: "gray", dimColor: true },
1256
+ !message.subAgentInternal && (React.createElement(Box, { flexDirection: "column" }, message.toolDisplay.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: theme.colors.menuSecondary, dimColor: true },
1255
1257
  arg.isLast ? '└─' : '├─',
1256
1258
  " ",
1257
1259
  arg.key,
@@ -1311,13 +1313,13 @@ export default function ChatScreen({ skipWelcome }) {
1311
1313
  message.content.includes('⚇✗')) &&
1312
1314
  message.content.includes('Tool execution rejected by user:') && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
1313
1315
  React.createElement(Text, { color: "yellow", dimColor: true }, "Rejection reason:"),
1314
- React.createElement(Text, { color: "gray", dimColor: true },
1316
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
1315
1317
  "\u2514\u2500",
1316
1318
  ' ',
1317
1319
  message.content
1318
1320
  .split('Tool execution rejected by user:')[1]
1319
1321
  ?.trim() || 'No reason provided'))),
1320
- message.files && message.files.length > 0 && (React.createElement(Box, { flexDirection: "column" }, message.files.map((file, fileIndex) => (React.createElement(Text, { key: fileIndex, color: "gray", dimColor: true },
1322
+ message.files && message.files.length > 0 && (React.createElement(Box, { flexDirection: "column" }, message.files.map((file, fileIndex) => (React.createElement(Text, { key: fileIndex, color: theme.colors.menuSecondary, dimColor: true },
1321
1323
  "\u2514\u2500 ",
1322
1324
  file.path,
1323
1325
  file.exists
@@ -1325,17 +1327,17 @@ export default function ChatScreen({ skipWelcome }) {
1325
1327
  : ' (file not found)'))))),
1326
1328
  message.role === 'user' &&
1327
1329
  message.images &&
1328
- message.images.length > 0 && (React.createElement(Box, { marginTop: 1, flexDirection: "column" }, message.images.map((_image, imageIndex) => (React.createElement(Text, { key: imageIndex, color: "gray", dimColor: true },
1330
+ message.images.length > 0 && (React.createElement(Box, { marginTop: 1, flexDirection: "column" }, message.images.map((_image, imageIndex) => (React.createElement(Text, { key: imageIndex, color: theme.colors.menuSecondary, dimColor: true },
1329
1331
  "\u2514\u2500 [image #",
1330
1332
  imageIndex + 1,
1331
1333
  "]"))))),
1332
1334
  message.discontinued && (React.createElement(Text, { color: "red", bold: true }, "\u2514\u2500 user discontinue")))))),
1333
1335
  isLastInGroup && (React.createElement(Box, { marginTop: 0 },
1334
- React.createElement(Text, { color: "#FF6EBF", dimColor: true }, "\u2514\u2500 End parallel execution")))));
1336
+ React.createElement(Text, { color: theme.colors.menuInfo, dimColor: true }, "\u2514\u2500 End parallel execution")))));
1335
1337
  }),
1336
1338
  ] }, item => item),
1337
1339
  (streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, paddingX: 1, width: terminalWidth },
1338
- React.createElement(Text, { color: ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][streamingState.animationFrame], bold: true }, "\u2746"),
1340
+ React.createElement(Text, { color: [theme.colors.menuInfo, theme.colors.success, theme.colors.menuSelected, theme.colors.menuInfo, theme.colors.menuSecondary][streamingState.animationFrame], bold: true }, "\u2746"),
1339
1341
  React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, streamingState.isStreaming ? (React.createElement(React.Fragment, null, streamingState.retryStatus &&
1340
1342
  streamingState.retryStatus.isRetrying ? (
1341
1343
  // Retry status display - hide "Thinking" and show retry info
@@ -1365,9 +1367,9 @@ export default function ChatScreen({ skipWelcome }) {
1365
1367
  "/",
1366
1368
  streamingState.codebaseSearchStatus.maxAttempts,
1367
1369
  ")"),
1368
- React.createElement(Text, { color: "gray", dimColor: true }, streamingState.codebaseSearchStatus.message))) : (
1370
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, streamingState.codebaseSearchStatus.message))) : (
1369
1371
  // Normal thinking status
1370
- React.createElement(Text, { color: "gray", dimColor: true },
1372
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
1371
1373
  React.createElement(ShimmerText, { text: streamingState.isReasoning
1372
1374
  ? t.chatScreen.statusDeepThinking
1373
1375
  : streamingState.streamTokenCount > 0
@@ -1385,7 +1387,7 @@ export default function ChatScreen({ skipWelcome }) {
1385
1387
  : streamingState.streamTokenCount,
1386
1388
  ' ',
1387
1389
  "tokens"),
1388
- ")")))) : (React.createElement(Text, { color: "gray", dimColor: true }, t.chatScreen.sessionCreating))))),
1390
+ ")")))) : (React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.sessionCreating))))),
1389
1391
  React.createElement(Box, { paddingX: 1, width: terminalWidth },
1390
1392
  React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
1391
1393
  pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames ||
@@ -1397,11 +1399,11 @@ export default function ChatScreen({ skipWelcome }) {
1397
1399
  showMcpPanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
1398
1400
  React.createElement(MCPInfoPanel, null),
1399
1401
  React.createElement(Box, { marginTop: 1 },
1400
- React.createElement(Text, { color: "gray", dimColor: true }, t.chatScreen.pressEscToClose)))),
1402
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.pressEscToClose)))),
1401
1403
  showUsagePanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
1402
1404
  React.createElement(UsagePanel, null),
1403
1405
  React.createElement(Box, { marginTop: 1 },
1404
- React.createElement(Text, { color: "gray", dimColor: true }, t.chatScreen.pressEscToClose)))),
1406
+ React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.pressEscToClose)))),
1405
1407
  showHelpPanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
1406
1408
  React.createElement(HelpPanel, null))),
1407
1409
  snapshotState.pendingRollback && (React.createElement(FileRollbackConfirmation, { fileCount: snapshotState.pendingRollback.fileCount, filePaths: snapshotState.pendingRollback.filePaths || [], onConfirm: handleRollbackConfirm })),
@@ -1428,7 +1430,7 @@ export default function ChatScreen({ skipWelcome }) {
1428
1430
  ? 'green'
1429
1431
  : vscodeState.vscodeConnectionStatus === 'error'
1430
1432
  ? 'red'
1431
- : 'gray', dimColor: vscodeState.vscodeConnectionStatus !== 'error' },
1433
+ : theme.colors.menuSecondary, dimColor: vscodeState.vscodeConnectionStatus !== 'error' },
1432
1434
  "\u25CF",
1433
1435
  ' ',
1434
1436
  vscodeState.vscodeConnectionStatus === 'connecting'