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,250 @@
|
|
|
1
|
+
import React, {useState, useEffect, useCallback} from 'react';
|
|
2
|
+
import {Text, useInput} from 'ink';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
interface TextPosition {
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CursorState {
|
|
11
|
+
cursorOffset: number;
|
|
12
|
+
desiredColumn: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 将字符偏移量转换为行列位置
|
|
17
|
+
*/
|
|
18
|
+
function offsetToPosition(text: string, offset: number): TextPosition {
|
|
19
|
+
let line = 0;
|
|
20
|
+
let column = 0;
|
|
21
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
22
|
+
if (text[i] === '\n') {
|
|
23
|
+
line++;
|
|
24
|
+
column = 0;
|
|
25
|
+
} else {
|
|
26
|
+
column++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {line, column};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 获取指定行的起始偏移量和行内容
|
|
34
|
+
*/
|
|
35
|
+
function getLineInfo(text: string, lineNumber: number): {startOffset: number; content: string} {
|
|
36
|
+
const lines = text.split('\n');
|
|
37
|
+
let offset = 0;
|
|
38
|
+
for (let i = 0; i < lineNumber && i < lines.length; i++) {
|
|
39
|
+
offset += lines[i].length + 1; // +1 for \n
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
startOffset: offset,
|
|
43
|
+
content: lines[lineNumber] ?? '',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 获取文本总行数
|
|
49
|
+
*/
|
|
50
|
+
function getLineCount(text: string): number {
|
|
51
|
+
if (text.length === 0) return 1;
|
|
52
|
+
return text.split('\n').length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function MultilineTextInput({
|
|
56
|
+
value: originalValue,
|
|
57
|
+
placeholder = '',
|
|
58
|
+
focus = true,
|
|
59
|
+
showCursor = true,
|
|
60
|
+
onChange,
|
|
61
|
+
onSubmit,
|
|
62
|
+
}: {
|
|
63
|
+
value: string;
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
focus?: boolean;
|
|
66
|
+
showCursor?: boolean;
|
|
67
|
+
onChange: (value: string) => void;
|
|
68
|
+
onSubmit?: (value: string) => void;
|
|
69
|
+
}): React.JSX.Element {
|
|
70
|
+
const [state, setState] = useState<CursorState>({
|
|
71
|
+
cursorOffset: (originalValue || '').length,
|
|
72
|
+
desiredColumn: null,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 外部 value 变化时,钳位光标位置
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setState(prev => {
|
|
78
|
+
if (!focus || !showCursor) return prev;
|
|
79
|
+
const maxOffset = (originalValue || '').length;
|
|
80
|
+
if (prev.cursorOffset > maxOffset) {
|
|
81
|
+
return {cursorOffset: maxOffset, desiredColumn: null};
|
|
82
|
+
}
|
|
83
|
+
return prev;
|
|
84
|
+
});
|
|
85
|
+
}, [originalValue, focus, showCursor]);
|
|
86
|
+
|
|
87
|
+
const {cursorOffset, desiredColumn} = state;
|
|
88
|
+
|
|
89
|
+
const handleKeyDown = useCallback((input: string, key: {
|
|
90
|
+
upArrow?: boolean;
|
|
91
|
+
downArrow?: boolean;
|
|
92
|
+
leftArrow?: boolean;
|
|
93
|
+
rightArrow?: boolean;
|
|
94
|
+
return?: boolean;
|
|
95
|
+
backspace?: boolean;
|
|
96
|
+
delete?: boolean;
|
|
97
|
+
ctrl?: boolean;
|
|
98
|
+
shift?: boolean;
|
|
99
|
+
tab?: boolean;
|
|
100
|
+
}) => {
|
|
101
|
+
// Ctrl 组合键:不插入字符
|
|
102
|
+
if (key.ctrl) {
|
|
103
|
+
if (input === 'u') {
|
|
104
|
+
onChange('');
|
|
105
|
+
setState({cursorOffset: 0, desiredColumn: null});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// 其他 Ctrl 组合键(c/o/x 等)不插入,让 App 层处理
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Tab 不处理(留给命令选择器)
|
|
113
|
+
if (key.tab) return;
|
|
114
|
+
|
|
115
|
+
// \n (Ctrl+J) 插入换行(终端中 \n 与 \r 是不同字节,可靠区分)
|
|
116
|
+
if (input === '\n') {
|
|
117
|
+
const nextValue = originalValue.slice(0, cursorOffset) + '\n' + originalValue.slice(cursorOffset);
|
|
118
|
+
onChange(nextValue);
|
|
119
|
+
setState({cursorOffset: cursorOffset + 1, desiredColumn: null});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Enter (\r) 提交(Shift+Enter 在大多数终端中与 Enter 发送相同的 \r,
|
|
124
|
+
// 无法区分,因此仅支持 Ctrl+J 换行)
|
|
125
|
+
if (key.return) {
|
|
126
|
+
onSubmit?.(originalValue);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 上箭头:移动到上一行的同列位置
|
|
131
|
+
if (key.upArrow) {
|
|
132
|
+
if (!showCursor) return;
|
|
133
|
+
const pos = offsetToPosition(originalValue, cursorOffset);
|
|
134
|
+
if (pos.line === 0) {
|
|
135
|
+
// 已经在第一行,移到行首
|
|
136
|
+
if (pos.column > 0) {
|
|
137
|
+
setState({cursorOffset: cursorOffset - pos.column, desiredColumn: null});
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const targetColumn = desiredColumn ?? pos.column;
|
|
142
|
+
const prevLine = getLineInfo(originalValue, pos.line - 1);
|
|
143
|
+
const newColumn = Math.min(targetColumn, prevLine.content.length);
|
|
144
|
+
setState({
|
|
145
|
+
cursorOffset: prevLine.startOffset + newColumn,
|
|
146
|
+
desiredColumn: targetColumn,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 下箭头:移动到下一行的同列位置
|
|
152
|
+
if (key.downArrow) {
|
|
153
|
+
if (!showCursor) return;
|
|
154
|
+
const totalLines = getLineCount(originalValue);
|
|
155
|
+
const pos = offsetToPosition(originalValue, cursorOffset);
|
|
156
|
+
if (pos.line >= totalLines - 1) {
|
|
157
|
+
// 已经在最后一行,移到行尾
|
|
158
|
+
if (cursorOffset < originalValue.length) {
|
|
159
|
+
setState({cursorOffset: originalValue.length, desiredColumn: null});
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const targetColumn = desiredColumn ?? pos.column;
|
|
164
|
+
const nextLine = getLineInfo(originalValue, pos.line + 1);
|
|
165
|
+
const newColumn = Math.min(targetColumn, nextLine.content.length);
|
|
166
|
+
setState({
|
|
167
|
+
cursorOffset: nextLine.startOffset + newColumn,
|
|
168
|
+
desiredColumn: targetColumn,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 左右箭头时清除 desiredColumn
|
|
174
|
+
if (key.leftArrow) {
|
|
175
|
+
if (showCursor && cursorOffset > 0) {
|
|
176
|
+
setState({cursorOffset: cursorOffset - 1, desiredColumn: null});
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (key.rightArrow) {
|
|
181
|
+
if (showCursor && cursorOffset < originalValue.length) {
|
|
182
|
+
setState({cursorOffset: cursorOffset + 1, desiredColumn: null});
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 退格/删除(统一处理:Windows Terminal 的 Backspace 发送 \x7f 被解析为 key.delete,
|
|
188
|
+
// 与 ink-text-input 保持一致,都删除光标前一个字符)
|
|
189
|
+
if (key.backspace || key.delete) {
|
|
190
|
+
if (cursorOffset > 0) {
|
|
191
|
+
const nextValue = originalValue.slice(0, cursorOffset - 1) + originalValue.slice(cursorOffset);
|
|
192
|
+
onChange(nextValue);
|
|
193
|
+
setState({cursorOffset: cursorOffset - 1, desiredColumn: null});
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 普通字符输入
|
|
199
|
+
if (input.length > 0) {
|
|
200
|
+
const nextValue = originalValue.slice(0, cursorOffset) + input + originalValue.slice(cursorOffset);
|
|
201
|
+
onChange(nextValue);
|
|
202
|
+
setState({cursorOffset: cursorOffset + input.length, desiredColumn: null});
|
|
203
|
+
}
|
|
204
|
+
}, [originalValue, cursorOffset, showCursor, onChange, onSubmit]);
|
|
205
|
+
|
|
206
|
+
useInput(handleKeyDown, {isActive: focus});
|
|
207
|
+
|
|
208
|
+
// --- 渲染 ---
|
|
209
|
+
const lines = originalValue.split('\n');
|
|
210
|
+
|
|
211
|
+
if (originalValue.length === 0 && placeholder) {
|
|
212
|
+
const renderedPlaceholder = showCursor && focus
|
|
213
|
+
? chalk.inverse(placeholder[0] ?? ' ') + chalk.grey(placeholder.slice(1))
|
|
214
|
+
: chalk.grey(placeholder);
|
|
215
|
+
return <Text>{renderedPlaceholder}</Text>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const renderedLines: string[] = [];
|
|
219
|
+
let runningOffset = 0;
|
|
220
|
+
|
|
221
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
222
|
+
const line = lines[lineIdx];
|
|
223
|
+
const lineStart = runningOffset;
|
|
224
|
+
const lineEnd = lineStart + line.length;
|
|
225
|
+
|
|
226
|
+
if (showCursor && focus && cursorOffset >= lineStart && cursorOffset <= lineEnd) {
|
|
227
|
+
// 光标在这一行
|
|
228
|
+
const cursorCol = cursorOffset - lineStart;
|
|
229
|
+
let rendered = '';
|
|
230
|
+
for (let i = 0; i < line.length; i++) {
|
|
231
|
+
rendered += i === cursorCol ? chalk.inverse(line[i]) : line[i];
|
|
232
|
+
}
|
|
233
|
+
if (cursorCol === line.length) {
|
|
234
|
+
rendered += chalk.inverse(' ');
|
|
235
|
+
}
|
|
236
|
+
renderedLines.push(rendered);
|
|
237
|
+
} else {
|
|
238
|
+
renderedLines.push(line || ' ');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
runningOffset = lineEnd + 1; // +1 for \n
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 空文本且无 placeholder
|
|
245
|
+
if (renderedLines.length === 0) {
|
|
246
|
+
renderedLines.push(showCursor && focus ? chalk.inverse(' ') : ' ');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return <Text>{renderedLines.join('\n')}</Text>;
|
|
250
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box} from 'ink';
|
|
3
|
+
|
|
4
|
+
import type {UiLanguage} from '../i18n.js';
|
|
5
|
+
import {t} from '../i18n.js';
|
|
6
|
+
import {useTheme} from '../theme/ThemeContext.js';
|
|
7
|
+
import {Spinner} from './Spinner.js';
|
|
8
|
+
import MultilineTextInput from './MultilineTextInput.js';
|
|
9
|
+
import type {TodoItemSnapshot} from '../types.js';
|
|
10
|
+
|
|
11
|
+
function noop(): void {}
|
|
12
|
+
|
|
13
|
+
function sanitizeInput(value: string): string {
|
|
14
|
+
// 仅移除回车符,保留换行符(多行)和空格
|
|
15
|
+
return value.replace(/\r/g, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function PromptInput({
|
|
19
|
+
busy,
|
|
20
|
+
input,
|
|
21
|
+
setInput,
|
|
22
|
+
onSubmit,
|
|
23
|
+
toolName,
|
|
24
|
+
suppressSubmit,
|
|
25
|
+
cursorReset,
|
|
26
|
+
language,
|
|
27
|
+
todoItems,
|
|
28
|
+
}: {
|
|
29
|
+
busy: boolean;
|
|
30
|
+
input: string;
|
|
31
|
+
setInput: (value: string) => void;
|
|
32
|
+
onSubmit: (value: string) => void;
|
|
33
|
+
toolName?: string;
|
|
34
|
+
suppressSubmit?: boolean;
|
|
35
|
+
cursorReset?: number;
|
|
36
|
+
language: UiLanguage;
|
|
37
|
+
todoItems?: TodoItemSnapshot[];
|
|
38
|
+
}): React.JSX.Element {
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
|
|
41
|
+
const handleChange = React.useCallback((value: string) => {
|
|
42
|
+
setInput(sanitizeInput(value));
|
|
43
|
+
}, [setInput]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box flexDirection="column" marginTop={1}>
|
|
47
|
+
{busy ? (
|
|
48
|
+
<Box marginBottom={1}>
|
|
49
|
+
<Spinner todoItems={todoItems} language={language} toolName={toolName} />
|
|
50
|
+
</Box>
|
|
51
|
+
) : null}
|
|
52
|
+
<Box borderStyle="round" borderColor={theme.colors.promptBorder} paddingLeft={1} paddingRight={1}>
|
|
53
|
+
<MultilineTextInput
|
|
54
|
+
key={cursorReset ?? 0}
|
|
55
|
+
value={input}
|
|
56
|
+
onChange={handleChange}
|
|
57
|
+
onSubmit={suppressSubmit ? noop : onSubmit}
|
|
58
|
+
placeholder={t(language, 'longTextHint')}
|
|
59
|
+
focus={!busy}
|
|
60
|
+
/>
|
|
61
|
+
</Box>
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
|
|
4
|
+
import {useTheme} from '../theme/ThemeContext.js';
|
|
5
|
+
|
|
6
|
+
export type SelectOption = {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
active?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const MAX_VISIBLE = 6;
|
|
14
|
+
|
|
15
|
+
export function SelectModal({
|
|
16
|
+
title,
|
|
17
|
+
options,
|
|
18
|
+
selectedIndex,
|
|
19
|
+
}: {
|
|
20
|
+
title: string;
|
|
21
|
+
options: SelectOption[];
|
|
22
|
+
selectedIndex: number;
|
|
23
|
+
}): React.JSX.Element {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
|
|
26
|
+
const startIndex = Math.max(
|
|
27
|
+
0,
|
|
28
|
+
Math.min(
|
|
29
|
+
selectedIndex - Math.floor(MAX_VISIBLE / 2),
|
|
30
|
+
options.length - MAX_VISIBLE,
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
const endIndex = Math.min(startIndex + MAX_VISIBLE, options.length);
|
|
34
|
+
const visible = options.slice(startIndex, endIndex);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Box flexDirection="column" marginTop={1}>
|
|
38
|
+
<Box>
|
|
39
|
+
<Text color={theme.colors.permission}>{theme.icons.pointer} </Text>
|
|
40
|
+
<Text bold>{title}</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
{visible.map((opt, vi) => {
|
|
43
|
+
const i = startIndex + vi;
|
|
44
|
+
const isSelected = i === selectedIndex;
|
|
45
|
+
const isCurrent = opt.active;
|
|
46
|
+
return (
|
|
47
|
+
<Box key={opt.value}>
|
|
48
|
+
<Text color={isSelected ? theme.colors.suggestion : theme.colors.muted}>
|
|
49
|
+
{isSelected ? `${theme.icons.pointer} ` : ' '}
|
|
50
|
+
</Text>
|
|
51
|
+
<Text color={isSelected ? theme.colors.suggestion : undefined} bold={isSelected} dimColor={!isSelected}>
|
|
52
|
+
{opt.label}
|
|
53
|
+
</Text>
|
|
54
|
+
{isCurrent ? (
|
|
55
|
+
<Box marginLeft={1}>
|
|
56
|
+
<Text color={theme.colors.success} dimColor>(current)</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
) : null}
|
|
59
|
+
{opt.description ? (
|
|
60
|
+
<Box marginLeft={1}>
|
|
61
|
+
<Text dimColor>{theme.icons.middleDot} {opt.description}</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
) : null}
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
<Box>
|
|
68
|
+
<Text dimColor>
|
|
69
|
+
<Text color={theme.colors.muted}>↑↓</Text> navigate
|
|
70
|
+
<Text> {theme.icons.middleDot} </Text>
|
|
71
|
+
<Text color={theme.colors.muted}>↵</Text> select
|
|
72
|
+
<Text> {theme.icons.middleDot} </Text>
|
|
73
|
+
<Text color={theme.colors.muted}>esc</Text> cancel
|
|
74
|
+
</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
|
|
4
|
+
import type {ThemeConfig} from '../theme/ThemeContext.js';
|
|
5
|
+
import {useTheme} from '../theme/ThemeContext.js';
|
|
6
|
+
import type {BridgeSessionSnapshot, McpServerSnapshot, TaskSnapshot} from '../types.js';
|
|
7
|
+
|
|
8
|
+
export function SidePanel({
|
|
9
|
+
status,
|
|
10
|
+
tasks,
|
|
11
|
+
commands,
|
|
12
|
+
commandHints,
|
|
13
|
+
mcpServers,
|
|
14
|
+
bridgeSessions,
|
|
15
|
+
}: {
|
|
16
|
+
status: Record<string, unknown>;
|
|
17
|
+
tasks: TaskSnapshot[];
|
|
18
|
+
commands: string[];
|
|
19
|
+
commandHints: string[];
|
|
20
|
+
mcpServers: McpServerSnapshot[];
|
|
21
|
+
bridgeSessions: BridgeSessionSnapshot[];
|
|
22
|
+
}): React.JSX.Element {
|
|
23
|
+
const theme = useTheme();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Box flexDirection="column" width="32%">
|
|
27
|
+
<StatusPanel status={status} theme={theme} />
|
|
28
|
+
<TaskPanel tasks={tasks} theme={theme} />
|
|
29
|
+
<McpPanel servers={mcpServers} theme={theme} />
|
|
30
|
+
<BridgePanel sessions={bridgeSessions} theme={theme} />
|
|
31
|
+
<CommandPanel commands={commands} hints={commandHints} theme={theme} />
|
|
32
|
+
</Box>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function StatusPanel({status, theme}: {status: Record<string, unknown>; theme: ThemeConfig}): React.JSX.Element {
|
|
37
|
+
const agentCount = Number(status.agent_count ?? 0);
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<Box marginBottom={1}>
|
|
41
|
+
<Text color={theme.colors.primary} bold>{theme.icons.chevron} Status</Text>
|
|
42
|
+
</Box>
|
|
43
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.muted} paddingX={1} marginBottom={1}>
|
|
44
|
+
<Text><Text dimColor>model:</Text> <Text color={theme.colors.accent}>{String(status.model ?? 'unknown')}</Text></Text>
|
|
45
|
+
{agentCount > 0 ? (
|
|
46
|
+
<Text><Text dimColor>agents:</Text> <Text color={theme.colors.illusion}>{agentCount} running</Text></Text>
|
|
47
|
+
) : null}
|
|
48
|
+
<Text><Text dimColor>provider:</Text> <Text color={theme.colors.accent}>{String(status.provider ?? 'unknown')}</Text></Text>
|
|
49
|
+
<Text><Text dimColor>auth:</Text> <Text color={theme.colors.accent}>{String(status.auth_status ?? 'unknown')}</Text></Text>
|
|
50
|
+
<Text><Text dimColor>permission:</Text> <Text color={theme.colors.accent}>{String(status.permission_mode ?? 'unknown')}</Text></Text>
|
|
51
|
+
<Text><Text dimColor>cwd:</Text> <Text color={theme.colors.accent}>{String(status.cwd ?? '.')}</Text></Text>
|
|
52
|
+
<Text><Text dimColor>language:</Text> <Text color={theme.colors.accent}>{String(status.ui_language ?? 'zh-CN')}</Text></Text>
|
|
53
|
+
<Text><Text dimColor>fast:</Text> <Text color={theme.colors.accent}>{String(Boolean(status.fast_mode))}</Text></Text>
|
|
54
|
+
<Text><Text dimColor>effort:</Text> <Text color={theme.colors.accent}>{String(status.effort ?? 'medium')}</Text></Text>
|
|
55
|
+
<Text><Text dimColor>passes:</Text> <Text color={theme.colors.accent}>{String(status.passes ?? 1)}</Text></Text>
|
|
56
|
+
</Box>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function TaskPanel({tasks, theme}: {tasks: TaskSnapshot[]; theme: ThemeConfig}): React.JSX.Element {
|
|
62
|
+
const visible = tasks.slice(0, 6);
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<Box marginBottom={1}>
|
|
66
|
+
<Text color={theme.colors.primary} bold>{theme.icons.chevron} Tasks</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.muted} paddingX={1} marginBottom={1}>
|
|
69
|
+
{visible.length === 0 ? (
|
|
70
|
+
<Text dimColor>(none)</Text>
|
|
71
|
+
) : (
|
|
72
|
+
visible.map((task) => (
|
|
73
|
+
<Box key={task.id} flexDirection="column" marginBottom={1}>
|
|
74
|
+
<Text>
|
|
75
|
+
<Text color={theme.colors.accent}>{task.id}</Text>
|
|
76
|
+
<Text dimColor> [{task.status}] </Text>
|
|
77
|
+
<Text>{task.description}</Text>
|
|
78
|
+
</Text>
|
|
79
|
+
<Text dimColor>
|
|
80
|
+
type={task.type} progress={task.metadata.progress ?? '-'} note={task.metadata.status_note ?? '-'}
|
|
81
|
+
</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
))
|
|
84
|
+
)}
|
|
85
|
+
</Box>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function McpPanel({servers, theme}: {servers: McpServerSnapshot[]; theme: ThemeConfig}): React.JSX.Element {
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<Box marginBottom={1}>
|
|
94
|
+
<Text color={theme.colors.primary} bold>{theme.icons.chevron} MCP</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.muted} paddingX={1} marginBottom={1}>
|
|
97
|
+
{servers.length === 0 ? (
|
|
98
|
+
<Text dimColor>(none)</Text>
|
|
99
|
+
) : (
|
|
100
|
+
servers.slice(0, 5).map((server) => (
|
|
101
|
+
<Box key={server.name} flexDirection="column" marginBottom={1}>
|
|
102
|
+
<Text>
|
|
103
|
+
<Text color={theme.colors.accent}>{server.name}</Text>
|
|
104
|
+
<Text dimColor> [{server.state}] </Text>
|
|
105
|
+
<Text>{server.transport ?? 'unknown'}</Text>
|
|
106
|
+
</Text>
|
|
107
|
+
<Text dimColor>
|
|
108
|
+
auth={String(Boolean(server.auth_configured))} tools={String(server.tool_count ?? 0)} resources=
|
|
109
|
+
{String(server.resource_count ?? 0)}
|
|
110
|
+
</Text>
|
|
111
|
+
{server.detail ? <Text dimColor>{server.detail}</Text> : null}
|
|
112
|
+
</Box>
|
|
113
|
+
))
|
|
114
|
+
)}
|
|
115
|
+
</Box>
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function BridgePanel({sessions, theme}: {sessions: BridgeSessionSnapshot[]; theme: ThemeConfig}): React.JSX.Element {
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<Box marginBottom={1}>
|
|
124
|
+
<Text color={theme.colors.primary} bold>{theme.icons.chevron} Bridge</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.muted} paddingX={1} marginBottom={1}>
|
|
127
|
+
{sessions.length === 0 ? (
|
|
128
|
+
<Text dimColor>(none)</Text>
|
|
129
|
+
) : (
|
|
130
|
+
sessions.slice(0, 4).map((session) => (
|
|
131
|
+
<Box key={session.session_id} flexDirection="column" marginBottom={1}>
|
|
132
|
+
<Text>
|
|
133
|
+
<Text color={theme.colors.accent}>{session.session_id}</Text>
|
|
134
|
+
<Text dimColor> [{session.status}] pid={session.pid}</Text>
|
|
135
|
+
</Text>
|
|
136
|
+
<Text dimColor>{session.command}</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
))
|
|
139
|
+
)}
|
|
140
|
+
</Box>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function CommandPanel({
|
|
146
|
+
commands,
|
|
147
|
+
hints,
|
|
148
|
+
theme,
|
|
149
|
+
}: {
|
|
150
|
+
commands: string[];
|
|
151
|
+
hints: string[];
|
|
152
|
+
theme: ThemeConfig;
|
|
153
|
+
}): React.JSX.Element {
|
|
154
|
+
return (
|
|
155
|
+
<>
|
|
156
|
+
<Box marginBottom={1}>
|
|
157
|
+
<Text color={theme.colors.primary} bold>{theme.icons.chevron} Commands</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.muted} paddingX={1}>
|
|
160
|
+
{hints.length > 0 ? (
|
|
161
|
+
hints.map((command, index) => (
|
|
162
|
+
<Text key={command} color={index === 0 ? theme.colors.accent : theme.colors.text}>
|
|
163
|
+
{command}
|
|
164
|
+
{index === 0 ? <Text dimColor> [tab]</Text> : ''}
|
|
165
|
+
</Text>
|
|
166
|
+
))
|
|
167
|
+
) : commands.length > 0 ? (
|
|
168
|
+
<Text dimColor>type / for commands</Text>
|
|
169
|
+
) : (
|
|
170
|
+
<Text dimColor>(none)</Text>
|
|
171
|
+
)}
|
|
172
|
+
</Box>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
|
|
4
|
+
import type {UiLanguage} from '../i18n.js';
|
|
5
|
+
import {t} from '../i18n.js';
|
|
6
|
+
import {useTheme} from '../theme/ThemeContext.js';
|
|
7
|
+
import type {TodoItemSnapshot} from '../types.js';
|
|
8
|
+
|
|
9
|
+
export function Spinner({label, todoItems, language, toolName, sessionId}: {label?: string; todoItems?: TodoItemSnapshot[]; language?: UiLanguage; toolName?: string; sessionId?: string}): React.JSX.Element {
|
|
10
|
+
const theme = useTheme();
|
|
11
|
+
const frames = theme.icons.spinner;
|
|
12
|
+
const [frame, setFrame] = useState(0);
|
|
13
|
+
const [verbIndex, setVerbIndex] = useState(0);
|
|
14
|
+
const [dotCount, setDotCount] = useState(0);
|
|
15
|
+
|
|
16
|
+
// 从 i18n 获取动词列表
|
|
17
|
+
const verbs = useMemo(() => {
|
|
18
|
+
if (!language) return ['Thinking'];
|
|
19
|
+
return t(language, 'spinnerVerbs').split(',');
|
|
20
|
+
}, [language]);
|
|
21
|
+
|
|
22
|
+
// 涟漪图标轮换
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const timer = setInterval(() => {
|
|
25
|
+
setFrame((f) => (f + 1) % frames.length);
|
|
26
|
+
}, 220);
|
|
27
|
+
return () => clearInterval(timer);
|
|
28
|
+
}, [frames.length]);
|
|
29
|
+
|
|
30
|
+
// 动词轮换
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const timer = setInterval(() => {
|
|
33
|
+
setVerbIndex((v) => (v + 1) % verbs.length);
|
|
34
|
+
}, 3000);
|
|
35
|
+
return () => clearInterval(timer);
|
|
36
|
+
}, [verbs.length]);
|
|
37
|
+
|
|
38
|
+
// 省略号呼吸动画:· → ·· → ··· → (空) → ·
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const timer = setInterval(() => {
|
|
41
|
+
setDotCount((d) => (d + 1) % 4);
|
|
42
|
+
}, 800);
|
|
43
|
+
return () => clearInterval(timer);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// 从todo列表中获取当前in_progress任务的activeForm
|
|
47
|
+
const currentTodo = todoItems?.find((t) => t.status === 'in_progress');
|
|
48
|
+
const nextTodo = todoItems?.find((t) => t.status === 'pending');
|
|
49
|
+
|
|
50
|
+
// 构建显示文本:优先使用 label,其次使用 todo activeForm,再次使用工具名,最后轮换动词
|
|
51
|
+
const verb = label ?? (currentTodo?.activeForm
|
|
52
|
+
? currentTodo.activeForm
|
|
53
|
+
: toolName && language
|
|
54
|
+
? `${t(language, 'spinnerToolAction')} ${toolName}`
|
|
55
|
+
: verbs[verbIndex]);
|
|
56
|
+
const dots = dotCount > 0 ? '·'.repeat(dotCount) : '';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box flexDirection="column">
|
|
60
|
+
<Box>
|
|
61
|
+
<Box width={2}>
|
|
62
|
+
<Text color={theme.colors.illusionShimmer}>{frames[frame]}</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
<Text color={theme.colors.illusionShimmer}>{verb}</Text>
|
|
65
|
+
<Box width={5}>
|
|
66
|
+
<Text color={theme.colors.illusionShimmer}> {dots}</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
{sessionId ? <Text color={theme.colors.muted} dimColor>(SESSION ID = {sessionId})</Text> : null}
|
|
69
|
+
</Box>
|
|
70
|
+
{nextTodo && !currentTodo ? (
|
|
71
|
+
<Box marginTop={1} marginLeft={3}>
|
|
72
|
+
<Text dimColor>Next: {nextTodo.content}</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
) : null}
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
}
|