illusion-code 0.1.0__py3-none-any.whl
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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
|
|
4
|
+
import {useTheme} from '../theme/ThemeContext.js';
|
|
5
|
+
|
|
6
|
+
// prettier-ignore
|
|
7
|
+
const LOGO = [
|
|
8
|
+
'████╗██╗ ██╗ ██╗ ██╗██████╗████╗ ████╗ ███╗ ██╗',
|
|
9
|
+
'╚██╔╝██║ ██║ ██║ ██║██╔═══╝╚██╔╝██║ ██║████╗ ██║',
|
|
10
|
+
' ██║ ██║ ██║ ██║ ██║██████╗ ██║ ██║ ██║██║██╗██║',
|
|
11
|
+
' ██║ ██║ ██║ ██║ ██║╚═══██║ ██║ ██║ ██║██║╚████║',
|
|
12
|
+
'████╗████╗████╗ ████╔╝██████║████╗ ████╔╝ ██║ ╚═██║',
|
|
13
|
+
'╚═══╝╚═══╝╚═══╝ ╚═══╝ ╚═════╝╚═══╝ ╚═══╝ ╚═╝ ╚═╝',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function WelcomeBanner({language}: {language?: string}): React.JSX.Element {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
21
|
+
<Box flexDirection="column">
|
|
22
|
+
{LOGO.map((line, i) => (
|
|
23
|
+
<Text key={i} color={theme.colors.primary} bold>{line}</Text>
|
|
24
|
+
))}
|
|
25
|
+
</Box>
|
|
26
|
+
<Box marginTop={1}>
|
|
27
|
+
<Text color={theme.colors.illusion} bold>{' Illusion Code · AI Coding Assistant'}</Text>
|
|
28
|
+
</Box>
|
|
29
|
+
<Box marginTop={1} flexDirection="column">
|
|
30
|
+
<Text dimColor>{` ${theme.icons.pointer} `}<Text color={theme.colors.suggestion}>/help</Text>{' view all commands'}</Text>
|
|
31
|
+
<Text dimColor>{` ${theme.icons.pointer} `}<Text color={theme.colors.suggestion}>/model</Text>{' switch model'}</Text>
|
|
32
|
+
<Text dimColor>{` ${theme.icons.pointer} `}<Text color={theme.colors.suggestion}>/resume</Text>{' resume session'}</Text>
|
|
33
|
+
<Text dimColor>{` ${theme.icons.pointer} `}<Text color={theme.colors.suggestion}>/language</Text>{' switch language'}</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
2
|
+
import {spawn, type ChildProcess} from 'node:child_process';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
BackendEvent,
|
|
7
|
+
BridgeSessionSnapshot,
|
|
8
|
+
FrontendConfig,
|
|
9
|
+
McpServerSnapshot,
|
|
10
|
+
PendingToolCall,
|
|
11
|
+
SelectRequestPayload,
|
|
12
|
+
SwarmNotificationSnapshot,
|
|
13
|
+
SwarmTeammateSnapshot,
|
|
14
|
+
TaskSnapshot,
|
|
15
|
+
TodoItemSnapshot,
|
|
16
|
+
TranscriptItem,
|
|
17
|
+
} from '../types.js';
|
|
18
|
+
|
|
19
|
+
const PROTOCOL_PREFIX = 'OHJSON:';
|
|
20
|
+
const ASSISTANT_DELTA_FLUSH_MS = 16;
|
|
21
|
+
const ASSISTANT_DELTA_FLUSH_CHARS = 32;
|
|
22
|
+
|
|
23
|
+
// Pattern for tool-call-like lines that the model may embed in assistant text.
|
|
24
|
+
// Matches: " bash (git add ...)" or "read (file_path: ...)"
|
|
25
|
+
const TOOL_CALL_LINE_RE = /^\s{2,}\w[\w-]*\s*\(.*\)\s*$/;
|
|
26
|
+
|
|
27
|
+
/** Strip lines that look like tool-call previews from assistant text. */
|
|
28
|
+
function stripToolCallLines(text: string): string {
|
|
29
|
+
const lines = text.split('\n');
|
|
30
|
+
const filtered = lines.filter((line) => !TOOL_CALL_LINE_RE.test(line));
|
|
31
|
+
// If stripping removed everything, return original text as fallback
|
|
32
|
+
return filtered.length > 0 ? filtered.join('\n') : text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useBackendSession(config: FrontendConfig, onExit: (code?: number | null) => void) {
|
|
36
|
+
const [staticItems, setStaticItems] = useState<TranscriptItem[]>([]);
|
|
37
|
+
const [clearCount, setClearCount] = useState(0);
|
|
38
|
+
const pushStatic = useCallback((item: TranscriptItem): void => {
|
|
39
|
+
setStaticItems((prev) => [...prev, item]);
|
|
40
|
+
}, []);
|
|
41
|
+
const [assistantBuffer, setAssistantBuffer] = useState('');
|
|
42
|
+
const [status, setStatus] = useState<Record<string, unknown>>({});
|
|
43
|
+
const [tasks, setTasks] = useState<TaskSnapshot[]>([]);
|
|
44
|
+
const [commands, setCommands] = useState<string[]>([]);
|
|
45
|
+
const [mcpServers, setMcpServers] = useState<McpServerSnapshot[]>([]);
|
|
46
|
+
const [bridgeSessions, setBridgeSessions] = useState<BridgeSessionSnapshot[]>([]);
|
|
47
|
+
const [modal, setModal] = useState<Record<string, unknown> | null>(null);
|
|
48
|
+
const [selectRequest, setSelectRequest] = useState<SelectRequestPayload | null>(null);
|
|
49
|
+
const [busy, setBusy] = useState(false);
|
|
50
|
+
const [ready, setReady] = useState(false);
|
|
51
|
+
const [showThinking, setShowThinking] = useState(true);
|
|
52
|
+
const [todoItems, setTodoItems] = useState<TodoItemSnapshot[]>([]);
|
|
53
|
+
const [pendingToolCalls, setPendingToolCalls] = useState<PendingToolCall[]>([]);
|
|
54
|
+
const [swarmTeammates, setSwarmTeammates] = useState<SwarmTeammateSnapshot[]>([]);
|
|
55
|
+
const [swarmNotifications, setSwarmNotifications] = useState<SwarmNotificationSnapshot[]>([]);
|
|
56
|
+
const [bgAgentLabel, setBgAgentLabel] = useState<string | null>(null);
|
|
57
|
+
const [commandResult, setCommandResult] = useState<{
|
|
58
|
+
text: string;
|
|
59
|
+
type: 'success' | 'error' | 'info';
|
|
60
|
+
} | null>(null);
|
|
61
|
+
const childRef = useRef<ChildProcess | null>(null);
|
|
62
|
+
const sentInitialPrompt = useRef(false);
|
|
63
|
+
|
|
64
|
+
// Streaming deltas can arrive one token at a time; updating Ink state for each
|
|
65
|
+
// delta causes heavy re-rendering/flicker. Buffer and flush at ~30fps.
|
|
66
|
+
const assistantBufferRef = useRef('');
|
|
67
|
+
const pendingAssistantDeltaRef = useRef('');
|
|
68
|
+
const assistantFlushTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
69
|
+
const reasoningBufferRef = useRef('');
|
|
70
|
+
// Raw buffer for thinking tag processing and final message text
|
|
71
|
+
const rawBufferRef = useRef('');
|
|
72
|
+
// Flag to prevent double-committing assistant text when tool_started
|
|
73
|
+
// flushes it before assistant_complete arrives
|
|
74
|
+
const assistantFlushedForToolRef = useRef(false);
|
|
75
|
+
// Ref for pendingToolCalls to avoid stale closure in handleEvent
|
|
76
|
+
const pendingToolCallsRef = useRef<PendingToolCall[]>([]);
|
|
77
|
+
|
|
78
|
+
const flushAssistantDelta = (): void => {
|
|
79
|
+
const pending = pendingAssistantDeltaRef.current;
|
|
80
|
+
if (!pending) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
pendingAssistantDeltaRef.current = '';
|
|
84
|
+
rawBufferRef.current += pending;
|
|
85
|
+
|
|
86
|
+
// Process thinking tags for streaming display
|
|
87
|
+
let displayText = rawBufferRef.current;
|
|
88
|
+
if (!showThinking) {
|
|
89
|
+
displayText = displayText
|
|
90
|
+
.replace(/<think\b[^>]*>[\s\S]*?<\/think\b[^>]*>/gi, '')
|
|
91
|
+
.replace(/<\/think\b[^>]*>/gi, '')
|
|
92
|
+
.replace(/<think\b[^>]*>/gi, '')
|
|
93
|
+
.replace(/<th(?:i(?:n(?:k)?)?)?\s*$/i, '');
|
|
94
|
+
} else {
|
|
95
|
+
displayText = displayText
|
|
96
|
+
.replace(/<think\b[^>]*>/gi, '')
|
|
97
|
+
.replace(/<\/think\b[^>]*>/gi, '')
|
|
98
|
+
.replace(/<th(?:i(?:n(?:k)?)?)?\s*$/i, '');
|
|
99
|
+
}
|
|
100
|
+
if (showThinking && reasoningBufferRef.current.trim()) {
|
|
101
|
+
const reasoning = reasoningBufferRef.current.trim();
|
|
102
|
+
const text = displayText.trim();
|
|
103
|
+
displayText = text ? `${reasoning}\n\n${text}` : reasoning;
|
|
104
|
+
}
|
|
105
|
+
assistantBufferRef.current = displayText;
|
|
106
|
+
setAssistantBuffer(displayText);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const clearAssistantDelta = (): void => {
|
|
110
|
+
pendingAssistantDeltaRef.current = '';
|
|
111
|
+
assistantBufferRef.current = '';
|
|
112
|
+
rawBufferRef.current = '';
|
|
113
|
+
if (assistantFlushTimerRef.current) {
|
|
114
|
+
clearTimeout(assistantFlushTimerRef.current);
|
|
115
|
+
assistantFlushTimerRef.current = null;
|
|
116
|
+
}
|
|
117
|
+
setAssistantBuffer('');
|
|
118
|
+
reasoningBufferRef.current = '';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sendRequest = (payload: Record<string, unknown>): void => {
|
|
122
|
+
const child = childRef.current;
|
|
123
|
+
if (!child || !child.stdin || child.stdin.destroyed) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
child.stdin.write(JSON.stringify(payload) + '\n');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clearStaticItems = (): void => {
|
|
130
|
+
setStaticItems([]);
|
|
131
|
+
setClearCount((c) => c + 1);
|
|
132
|
+
clearAssistantDelta();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const [command, ...args] = config.backend_command;
|
|
137
|
+
const child = spawn(command, args, {
|
|
138
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
139
|
+
env: process.env,
|
|
140
|
+
detached: true,
|
|
141
|
+
windowsHide: true,
|
|
142
|
+
});
|
|
143
|
+
childRef.current = child;
|
|
144
|
+
|
|
145
|
+
const reader = readline.createInterface({input: child.stdout});
|
|
146
|
+
reader.on('line', (line) => {
|
|
147
|
+
if (!line.startsWith(PROTOCOL_PREFIX)) {
|
|
148
|
+
pushStatic({role: 'log', text: line});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const event = JSON.parse(line.slice(PROTOCOL_PREFIX.length)) as BackendEvent;
|
|
152
|
+
handleEvent(event);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
child.on('exit', (code) => {
|
|
156
|
+
pushStatic({role: 'system', text: `backend exited with code ${code ?? 0}`});
|
|
157
|
+
process.exitCode = code ?? 0;
|
|
158
|
+
onExit(code);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Ensure child processes are killed on parent exit (prevents stale processes)
|
|
162
|
+
const killChild = (): void => {
|
|
163
|
+
if (!child.killed) {
|
|
164
|
+
// Kill process group to ensure Python backend and its children all die
|
|
165
|
+
try {
|
|
166
|
+
if (child.pid) {
|
|
167
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
child.kill('SIGTERM');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (assistantFlushTimerRef.current) {
|
|
174
|
+
clearTimeout(assistantFlushTimerRef.current);
|
|
175
|
+
assistantFlushTimerRef.current = null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
process.on('exit', killChild);
|
|
179
|
+
process.on('SIGINT', killChild);
|
|
180
|
+
process.on('SIGTERM', killChild);
|
|
181
|
+
|
|
182
|
+
return () => {
|
|
183
|
+
reader.close();
|
|
184
|
+
killChild();
|
|
185
|
+
process.removeListener('exit', killChild);
|
|
186
|
+
process.removeListener('SIGINT', killChild);
|
|
187
|
+
process.removeListener('SIGTERM', killChild);
|
|
188
|
+
};
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const handleEvent = (event: BackendEvent): void => {
|
|
192
|
+
if (event.type === 'ready') {
|
|
193
|
+
setReady(true);
|
|
194
|
+
setStatus(event.state ?? {});
|
|
195
|
+
const showThinkingFromState = event.state?.show_thinking;
|
|
196
|
+
if (typeof showThinkingFromState === 'boolean') {
|
|
197
|
+
setShowThinking(showThinkingFromState);
|
|
198
|
+
}
|
|
199
|
+
setTasks(event.tasks ?? []);
|
|
200
|
+
setCommands(event.commands ?? []);
|
|
201
|
+
setMcpServers(event.mcp_servers ?? []);
|
|
202
|
+
setBridgeSessions(event.bridge_sessions ?? []);
|
|
203
|
+
if (config.initial_prompt && !sentInitialPrompt.current) {
|
|
204
|
+
sentInitialPrompt.current = true;
|
|
205
|
+
sendRequest({type: 'submit_line', line: config.initial_prompt});
|
|
206
|
+
setBusy(true);
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (event.type === 'state_snapshot') {
|
|
211
|
+
setStatus(event.state ?? {});
|
|
212
|
+
const showThinkingFromState = event.state?.show_thinking;
|
|
213
|
+
if (typeof showThinkingFromState === 'boolean') {
|
|
214
|
+
setShowThinking(showThinkingFromState);
|
|
215
|
+
}
|
|
216
|
+
setMcpServers(event.mcp_servers ?? []);
|
|
217
|
+
setBridgeSessions(event.bridge_sessions ?? []);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (event.type === 'tasks_snapshot') {
|
|
221
|
+
setTasks(event.tasks ?? []);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (event.type === 'transcript_item' && event.item) {
|
|
225
|
+
pushStatic(event.item as TranscriptItem);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (event.type === 'assistant_delta') {
|
|
229
|
+
assistantFlushedForToolRef.current = false;
|
|
230
|
+
if (event.reasoning) {
|
|
231
|
+
reasoningBufferRef.current += event.reasoning;
|
|
232
|
+
}
|
|
233
|
+
const delta = event.message ?? '';
|
|
234
|
+
if (!delta) {
|
|
235
|
+
if (showThinking && reasoningBufferRef.current.trim()) {
|
|
236
|
+
const display = reasoningBufferRef.current.trim();
|
|
237
|
+
assistantBufferRef.current = display;
|
|
238
|
+
setAssistantBuffer(display);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
pendingAssistantDeltaRef.current += delta;
|
|
243
|
+
if (pendingAssistantDeltaRef.current.length >= ASSISTANT_DELTA_FLUSH_CHARS) {
|
|
244
|
+
flushAssistantDelta();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!assistantFlushTimerRef.current) {
|
|
248
|
+
assistantFlushTimerRef.current = setTimeout(() => {
|
|
249
|
+
assistantFlushTimerRef.current = null;
|
|
250
|
+
flushAssistantDelta();
|
|
251
|
+
}, ASSISTANT_DELTA_FLUSH_MS);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (event.type === 'assistant_complete') {
|
|
256
|
+
if (assistantFlushTimerRef.current) {
|
|
257
|
+
clearTimeout(assistantFlushTimerRef.current);
|
|
258
|
+
assistantFlushTimerRef.current = null;
|
|
259
|
+
}
|
|
260
|
+
flushAssistantDelta();
|
|
261
|
+
|
|
262
|
+
// Skip if tool_started already committed this text to static
|
|
263
|
+
if (!assistantFlushedForToolRef.current) {
|
|
264
|
+
const text = event.message ?? rawBufferRef.current;
|
|
265
|
+
const reasoning = (event.reasoning ?? reasoningBufferRef.current) || undefined;
|
|
266
|
+
if (text.trim() || (reasoning ?? '').trim()) {
|
|
267
|
+
pushStatic({role: 'assistant', text: stripToolCallLines(text), reasoning});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
assistantFlushedForToolRef.current = false;
|
|
271
|
+
|
|
272
|
+
clearAssistantDelta();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (event.type === 'line_complete') {
|
|
276
|
+
// If the line ended without an assistant_complete (e.g. errors), make sure we
|
|
277
|
+
// don't leave stale streaming text on screen.
|
|
278
|
+
clearAssistantDelta();
|
|
279
|
+
pendingToolCallsRef.current = [];
|
|
280
|
+
setPendingToolCalls([]);
|
|
281
|
+
setBgAgentLabel(null);
|
|
282
|
+
setBusy(false);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if ((event.type === 'tool_started' || event.type === 'tool_completed') && event.item) {
|
|
286
|
+
if (event.type === 'tool_started') {
|
|
287
|
+
// Commit any pending assistant text to static before the tool call appears
|
|
288
|
+
if (rawBufferRef.current.trim() || pendingAssistantDeltaRef.current || reasoningBufferRef.current.trim()) {
|
|
289
|
+
if (assistantFlushTimerRef.current) {
|
|
290
|
+
clearTimeout(assistantFlushTimerRef.current);
|
|
291
|
+
assistantFlushTimerRef.current = null;
|
|
292
|
+
}
|
|
293
|
+
flushAssistantDelta();
|
|
294
|
+
const text = rawBufferRef.current;
|
|
295
|
+
const reasoning = reasoningBufferRef.current || undefined;
|
|
296
|
+
if (text.trim() || (reasoning ?? '').trim()) {
|
|
297
|
+
pushStatic({
|
|
298
|
+
role: 'assistant',
|
|
299
|
+
text: stripToolCallLines(text),
|
|
300
|
+
reasoning,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
clearAssistantDelta();
|
|
304
|
+
assistantFlushedForToolRef.current = true;
|
|
305
|
+
}
|
|
306
|
+
setBusy(true);
|
|
307
|
+
// 工具调用全过程保持在 pendingToolCalls 状态(非 Static),
|
|
308
|
+
// 以便对 ● 做闪烁动画,直到 tool_completed 才推入 staticItems
|
|
309
|
+
const toolInput = event.item.tool_input ?? event.tool_input;
|
|
310
|
+
const toolUseId = event.item.tool_use_id ?? event.tool_use_id ?? '';
|
|
311
|
+
const pendingCall: PendingToolCall = {
|
|
312
|
+
tool_name: event.item.tool_name ?? event.tool_name ?? 'tool',
|
|
313
|
+
tool_use_id: toolUseId,
|
|
314
|
+
tool_input: (toolInput && Object.keys(toolInput).length > 0) ? toolInput : undefined,
|
|
315
|
+
};
|
|
316
|
+
pendingToolCallsRef.current = [...pendingToolCallsRef.current, pendingCall];
|
|
317
|
+
setPendingToolCalls(pendingToolCallsRef.current);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// tool_completed: 将工具项和结果一并推入 staticItems
|
|
321
|
+
if (event.type === 'tool_completed') {
|
|
322
|
+
const toolUseId = event.item.tool_use_id ?? event.tool_use_id ?? '';
|
|
323
|
+
const pendingIdx = pendingToolCallsRef.current.findIndex(p => p.tool_use_id === toolUseId);
|
|
324
|
+
if (pendingIdx !== -1) {
|
|
325
|
+
const pending = pendingToolCallsRef.current[pendingIdx];
|
|
326
|
+
pendingToolCallsRef.current = pendingToolCallsRef.current.filter(p => p.tool_use_id !== toolUseId);
|
|
327
|
+
setPendingToolCalls(pendingToolCallsRef.current);
|
|
328
|
+
pushStatic({
|
|
329
|
+
role: 'tool',
|
|
330
|
+
text: pending.tool_name,
|
|
331
|
+
tool_name: pending.tool_name,
|
|
332
|
+
tool_input: pending.tool_input,
|
|
333
|
+
tool_use_id: pending.tool_use_id || undefined,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const enrichedItem: TranscriptItem = {
|
|
337
|
+
...event.item,
|
|
338
|
+
tool_name: event.item.tool_name ?? event.tool_name ?? undefined,
|
|
339
|
+
tool_input: event.item.tool_input ?? undefined,
|
|
340
|
+
tool_use_id: event.item.tool_use_id ?? event.tool_use_id ?? undefined,
|
|
341
|
+
is_error: event.item.is_error ?? event.is_error ?? undefined,
|
|
342
|
+
};
|
|
343
|
+
pushStatic(enrichedItem);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (event.type === 'tool_input_updated') {
|
|
348
|
+
// 后端发送了完整的工具参数,更新 pendingToolCalls 中对应项的 tool_input
|
|
349
|
+
const toolUseId = event.tool_use_id;
|
|
350
|
+
pendingToolCallsRef.current = pendingToolCallsRef.current.map(p =>
|
|
351
|
+
p.tool_use_id === toolUseId
|
|
352
|
+
? {...p, tool_input: event.tool_input ?? undefined}
|
|
353
|
+
: p,
|
|
354
|
+
);
|
|
355
|
+
setPendingToolCalls(pendingToolCallsRef.current);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (event.type === 'clear_transcript') {
|
|
359
|
+
setStaticItems([]);
|
|
360
|
+
setClearCount((c) => c + 1);
|
|
361
|
+
clearAssistantDelta();
|
|
362
|
+
pendingToolCallsRef.current = [];
|
|
363
|
+
setPendingToolCalls([]);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (event.type === 'replace_transcript' && event.items) {
|
|
367
|
+
const newItems = (event.items as TranscriptItem[]).filter((item: TranscriptItem) => {
|
|
368
|
+
if (item.role === 'user' && item.text.startsWith('/')) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
});
|
|
373
|
+
setStaticItems(newItems);
|
|
374
|
+
setClearCount((c) => c + 1);
|
|
375
|
+
clearAssistantDelta();
|
|
376
|
+
pendingToolCallsRef.current = [];
|
|
377
|
+
setPendingToolCalls([]);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (event.type === 'select_request') {
|
|
381
|
+
const m = event.modal ?? {};
|
|
382
|
+
setSelectRequest({
|
|
383
|
+
title: String(m.title ?? 'Select'),
|
|
384
|
+
command: String(m.command ?? ''),
|
|
385
|
+
options: event.select_options ?? [],
|
|
386
|
+
});
|
|
387
|
+
setBusy(false);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (event.type === 'modal_request') {
|
|
391
|
+
setModal(event.modal ?? null);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (event.type === 'error') {
|
|
395
|
+
pushStatic({role: 'system', text: `error: ${event.message ?? 'unknown error'}`});
|
|
396
|
+
clearAssistantDelta();
|
|
397
|
+
setBusy(false);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (event.type === 'todo_update') {
|
|
401
|
+
if (event.todo_items != null) {
|
|
402
|
+
setTodoItems(event.todo_items);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (event.type === 'swarm_status') {
|
|
407
|
+
if (event.swarm_teammates != null) {
|
|
408
|
+
setSwarmTeammates(event.swarm_teammates);
|
|
409
|
+
}
|
|
410
|
+
if (event.swarm_notifications != null) {
|
|
411
|
+
setSwarmNotifications((prev) => [...prev, ...event.swarm_notifications!].slice(-20));
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (event.type === 'plan_mode_change') {
|
|
416
|
+
if (event.plan_mode != null) {
|
|
417
|
+
setStatus((s) => ({...s, permission_mode: event.plan_mode}));
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (event.type === 'command_result' && event.command_result_data) {
|
|
422
|
+
setCommandResult({
|
|
423
|
+
text: event.command_result_data.message,
|
|
424
|
+
type: event.command_result_data.type || 'info',
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (event.type === 'bg_agent_status') {
|
|
429
|
+
setBgAgentLabel(event.message ?? null);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (event.type === 'shutdown') {
|
|
433
|
+
onExit(0);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
return useMemo(
|
|
438
|
+
() => ({
|
|
439
|
+
staticItems,
|
|
440
|
+
assistantBuffer,
|
|
441
|
+
clearCount,
|
|
442
|
+
showThinking,
|
|
443
|
+
status,
|
|
444
|
+
tasks,
|
|
445
|
+
commands,
|
|
446
|
+
mcpServers,
|
|
447
|
+
bridgeSessions,
|
|
448
|
+
modal,
|
|
449
|
+
selectRequest,
|
|
450
|
+
busy,
|
|
451
|
+
ready,
|
|
452
|
+
todoItems,
|
|
453
|
+
pendingToolCalls,
|
|
454
|
+
swarmTeammates,
|
|
455
|
+
swarmNotifications,
|
|
456
|
+
bgAgentLabel,
|
|
457
|
+
commandResult,
|
|
458
|
+
setCommandResult,
|
|
459
|
+
setModal,
|
|
460
|
+
setSelectRequest,
|
|
461
|
+
setBusy,
|
|
462
|
+
sendRequest,
|
|
463
|
+
clearStaticItems,
|
|
464
|
+
pushStatic,
|
|
465
|
+
}),
|
|
466
|
+
[assistantBuffer, bridgeSessions, busy, clearCount, commandResult, commands, mcpServers, modal, pendingToolCalls, ready, selectRequest, showThinking, staticItems, status, swarmNotifications, swarmTeammates, tasks, todoItems, bgAgentLabel]
|
|
467
|
+
);
|
|
468
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type UiLanguage = 'zh-CN' | 'en';
|
|
2
|
+
|
|
3
|
+
type Dict = Record<string, string>;
|
|
4
|
+
|
|
5
|
+
const ZH: Dict = {
|
|
6
|
+
connecting: '正在连接后端...',
|
|
7
|
+
send: '发送',
|
|
8
|
+
commands: '命令',
|
|
9
|
+
exit: '退出',
|
|
10
|
+
exitProgram: '退出程序',
|
|
11
|
+
stopCurrentTask: '停止当前任务',
|
|
12
|
+
permissionMode: '权限模式',
|
|
13
|
+
language: '语言',
|
|
14
|
+
langZh: '简体中文',
|
|
15
|
+
langEn: 'English',
|
|
16
|
+
newline: '换行',
|
|
17
|
+
allow: '允许',
|
|
18
|
+
alwaysAllow: '总是允许',
|
|
19
|
+
deny: '拒绝',
|
|
20
|
+
welcomeSub: 'AI 编码助手',
|
|
21
|
+
statusReady: '就绪',
|
|
22
|
+
statusThinking: '思考中...',
|
|
23
|
+
statusExecuting: '执行指令中...',
|
|
24
|
+
statusToolPrefix: '执行工具',
|
|
25
|
+
spinnerVerbs: '酝酿,生发,铺陈,点染,贯通,渲染,独照,澄明',
|
|
26
|
+
spinnerToolAction: '正在执行',
|
|
27
|
+
longTextHint: '多段需求或长文本建议写入文档后命 illusion code 读取',
|
|
28
|
+
clearInput: '清空输入',
|
|
29
|
+
taskStopped: '当前任务已停止。',
|
|
30
|
+
reasoning: '思考过程',
|
|
31
|
+
assistantReply: '助手回复',
|
|
32
|
+
bgAgentWaiting: '等待后台代理完成',
|
|
33
|
+
bgAgentResuming: '后台代理已完成,继续执行',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const EN: Dict = {
|
|
37
|
+
connecting: 'Connecting to backend...',
|
|
38
|
+
send: 'send',
|
|
39
|
+
commands: 'commands',
|
|
40
|
+
exit: 'exit',
|
|
41
|
+
exitProgram: 'exit program',
|
|
42
|
+
stopCurrentTask: 'stop current task',
|
|
43
|
+
permissionMode: 'Permission Mode',
|
|
44
|
+
language: 'Language',
|
|
45
|
+
langZh: '简体中文',
|
|
46
|
+
langEn: 'English',
|
|
47
|
+
newline: 'newline',
|
|
48
|
+
allow: 'Allow',
|
|
49
|
+
alwaysAllow: 'Always Allow',
|
|
50
|
+
deny: 'Deny',
|
|
51
|
+
welcomeSub: 'An AI-powered coding assistant',
|
|
52
|
+
statusReady: 'Ready',
|
|
53
|
+
statusThinking: 'Thinking...',
|
|
54
|
+
statusExecuting: 'Executing command...',
|
|
55
|
+
statusToolPrefix: 'Running tool',
|
|
56
|
+
spinnerVerbs: 'Thinking,Processing,Analyzing,Reasoning,Generating,Deliberating,Crafting,Refining,Computing,Synthesizing',
|
|
57
|
+
spinnerToolAction: 'Running',
|
|
58
|
+
longTextHint: 'For complex or long text, write to doc and let illusion code read it',
|
|
59
|
+
clearInput: 'clear input',
|
|
60
|
+
taskStopped: 'Current task stopped.',
|
|
61
|
+
reasoning: 'Thinking',
|
|
62
|
+
assistantReply: 'Response',
|
|
63
|
+
bgAgentWaiting: 'Waiting for background agent',
|
|
64
|
+
bgAgentResuming: 'Background agent completed, resuming',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const ALL: Record<UiLanguage, Dict> = {
|
|
68
|
+
'zh-CN': ZH,
|
|
69
|
+
en: EN,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function normalizeLanguage(raw: unknown): UiLanguage {
|
|
73
|
+
return raw === 'en' ? 'en' : 'zh-CN';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function t(lang: UiLanguage, key: keyof typeof ZH): string {
|
|
77
|
+
return ALL[lang][key] ?? ZH[key];
|
|
78
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {render} from 'ink';
|
|
3
|
+
|
|
4
|
+
import {App} from './App.js';
|
|
5
|
+
import type {FrontendConfig} from './types.js';
|
|
6
|
+
|
|
7
|
+
const config = JSON.parse(process.env.ILLUSION_FRONTEND_CONFIG ?? '{}') as FrontendConfig;
|
|
8
|
+
|
|
9
|
+
// Restore terminal cursor visibility on exit (Ink hides it by default)
|
|
10
|
+
const restoreCursor = (): void => {
|
|
11
|
+
process.stdout.write('\x1B[?25h');
|
|
12
|
+
};
|
|
13
|
+
process.on('exit', restoreCursor);
|
|
14
|
+
// SIGINT 由 App 组件中的 useInput 处理,不再强制退出
|
|
15
|
+
// 仅在无法恢复时作为安全网退出
|
|
16
|
+
process.on('SIGTERM', () => {
|
|
17
|
+
restoreCursor();
|
|
18
|
+
process.exit(143);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// --- Suppress resize events ---
|
|
22
|
+
// ink's eraseLines(N) + output pattern causes visible flicker on every resize.
|
|
23
|
+
// But ink recalculates layout on EVERY React re-render (reading stdout.columns),
|
|
24
|
+
// not just on resize events. So we can safely suppress resize events entirely.
|
|
25
|
+
//
|
|
26
|
+
// Layout still updates correctly when:
|
|
27
|
+
// - User types (PromptInput state change → React re-render → layout recalc)
|
|
28
|
+
// - Spinner ticks during busy state (32ms interval → React re-render)
|
|
29
|
+
// - Backend sends events (status changes → React re-render)
|
|
30
|
+
//
|
|
31
|
+
// The only effect: terminal content doesn't reflow on resize until the next
|
|
32
|
+
// React re-render. This is acceptable because idle content (StatusBar, hints)
|
|
33
|
+
// is short and reflows on the next user interaction.
|
|
34
|
+
const _origEmit = process.stdout.emit.bind(process.stdout);
|
|
35
|
+
process.stdout.emit = function (event: string, ...args: unknown[]) {
|
|
36
|
+
if (event === 'resize') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return _origEmit(event, ...args);
|
|
40
|
+
} as typeof process.stdout.emit;
|
|
41
|
+
|
|
42
|
+
render(<App config={config} />);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, {createContext, useContext} from 'react';
|
|
2
|
+
|
|
3
|
+
import {type ThemeConfig, defaultTheme} from './builtinThemes.js';
|
|
4
|
+
|
|
5
|
+
export type {ThemeConfig};
|
|
6
|
+
|
|
7
|
+
const ThemeContext = createContext<ThemeConfig>(defaultTheme);
|
|
8
|
+
|
|
9
|
+
export function ThemeProvider({children}: {children: React.ReactNode}): React.JSX.Element {
|
|
10
|
+
return (
|
|
11
|
+
<ThemeContext.Provider value={defaultTheme}>
|
|
12
|
+
{children}
|
|
13
|
+
</ThemeContext.Provider>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useTheme(): ThemeConfig {
|
|
18
|
+
return useContext(ThemeContext);
|
|
19
|
+
}
|