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.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,245 @@
1
+ import type {Token, Tokens} from 'marked';
2
+ import React from 'react';
3
+ import {Text} from 'ink';
4
+ import {useTerminalSize} from '../hooks/useTerminalSize.js';
5
+ import {padAligned, stringWidth, stripAnsi, wrapText} from '../utils/markdown.js';
6
+
7
+ const SAFETY_MARGIN = 4;
8
+ const MIN_COLUMN_WIDTH = 3;
9
+ const MAX_ROW_LINES = 4;
10
+
11
+ const INLINE_CODE_COLOR = '#b1b9f9';
12
+
13
+ function hexToAnsiRgb(hex: string): string {
14
+ const r = parseInt(hex.slice(1, 3), 16);
15
+ const g = parseInt(hex.slice(3, 5), 16);
16
+ const b = parseInt(hex.slice(5, 7), 16);
17
+ return `38;2;${r};${g};${b}`;
18
+ }
19
+
20
+ function renderInlineToAnsi(tokens: Token[] | undefined): string {
21
+ if (!tokens || tokens.length === 0) return '';
22
+ let result = '';
23
+
24
+ for (const t of tokens) {
25
+ switch (t.type) {
26
+ case 'strong': {
27
+ const st = t as Tokens.Strong;
28
+ result += `\x1b[1m${renderInlineToAnsi(st.tokens)}\x1b[22m`;
29
+ break;
30
+ }
31
+ case 'em': {
32
+ const et = t as Tokens.Em;
33
+ result += `\x1b[3m${renderInlineToAnsi(et.tokens)}\x1b[23m`;
34
+ break;
35
+ }
36
+ case 'codespan': {
37
+ const ct = t as Tokens.Codespan;
38
+ result += `\x1b[${hexToAnsiRgb(INLINE_CODE_COLOR)}m${ct.text}\x1b[39m`;
39
+ break;
40
+ }
41
+ case 'link': {
42
+ const lt = t as Tokens.Link;
43
+ result += `\x1b[4m${renderInlineToAnsi(lt.tokens)}\x1b[24m`;
44
+ break;
45
+ }
46
+ case 'text': {
47
+ const tt = t as Tokens.Text;
48
+ if (tt.tokens && tt.tokens.length > 0) {
49
+ result += renderInlineToAnsi(tt.tokens);
50
+ } else {
51
+ result += tt.raw ?? tt.text;
52
+ }
53
+ break;
54
+ }
55
+ case 'escape': {
56
+ result += t.text;
57
+ break;
58
+ }
59
+ default: {
60
+ result += (t as {raw?: string}).raw ?? (t as {text?: string}).text ?? '';
61
+ break;
62
+ }
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ type CellData = {text: string; tokens: Token[]};
70
+
71
+ type Props = {
72
+ token: Tokens.Table;
73
+ forceWidth?: number;
74
+ };
75
+
76
+ function cellAnsiText(cell: CellData | undefined | null): string {
77
+ if (!cell) return '';
78
+ if (!cell.tokens || cell.tokens.length === 0) return cell.text ?? '';
79
+ return renderInlineToAnsi(cell.tokens);
80
+ }
81
+
82
+ function cellPlainText(cell: CellData | undefined | null): string {
83
+ return stripAnsi(cellAnsiText(cell));
84
+ }
85
+
86
+ function cellMinWidth(cell: CellData | undefined | null): number {
87
+ const text = cellPlainText(cell);
88
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
89
+ if (words.length === 0) return MIN_COLUMN_WIDTH;
90
+ return Math.max(...words.map((w) => stringWidth(w)), MIN_COLUMN_WIDTH);
91
+ }
92
+
93
+ function cellIdealWidth(cell: CellData | undefined | null): number {
94
+ return Math.max(stringWidth(cellPlainText(cell)), MIN_COLUMN_WIDTH);
95
+ }
96
+
97
+ export function MarkdownTable({token, forceWidth}: Props): React.JSX.Element {
98
+ const {columns: actualTerminalWidth} = useTerminalSize();
99
+ const terminalWidth = forceWidth ?? actualTerminalWidth;
100
+
101
+ const headerCells = token.header as unknown as CellData[];
102
+ const rowsCells = token.rows as unknown as CellData[][];
103
+
104
+ const minWidths = headerCells.map((_h, colIndex) => {
105
+ let mw = cellMinWidth(headerCells[colIndex]);
106
+ for (const row of rowsCells) {
107
+ mw = Math.max(mw, cellMinWidth(row[colIndex]));
108
+ }
109
+ return mw;
110
+ });
111
+ const idealWidths = headerCells.map((_h, colIndex) => {
112
+ let iw = cellIdealWidth(headerCells[colIndex]);
113
+ for (const row of rowsCells) {
114
+ iw = Math.max(iw, cellIdealWidth(row[colIndex]));
115
+ }
116
+ return iw;
117
+ });
118
+
119
+ const numCols = headerCells.length;
120
+ const borderOverhead = 1 + numCols * 3;
121
+ const availableWidth = Math.max(terminalWidth - borderOverhead - SAFETY_MARGIN, numCols * MIN_COLUMN_WIDTH);
122
+
123
+ const totalMin = minWidths.reduce((s, w) => s + w, 0);
124
+ const totalIdeal = idealWidths.reduce((s, w) => s + w, 0);
125
+
126
+ let needsHardWrap = false;
127
+ let columnWidths: number[];
128
+ if (totalIdeal <= availableWidth) {
129
+ columnWidths = idealWidths;
130
+ } else if (totalMin <= availableWidth) {
131
+ const extraSpace = availableWidth - totalMin;
132
+ const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]!);
133
+ const totalOverflow = overflows.reduce((s, o) => s + o, 0);
134
+ columnWidths = minWidths.map((min, i) => {
135
+ if (totalOverflow === 0) return min;
136
+ return min + Math.floor((overflows[i]! / totalOverflow) * extraSpace);
137
+ });
138
+ } else {
139
+ needsHardWrap = true;
140
+ const scaleFactor = availableWidth / totalMin;
141
+ columnWidths = minWidths.map((w) => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH));
142
+ }
143
+
144
+ function renderRowLines(cells: Array<CellData | undefined>, isHeader: boolean): string[] {
145
+ const cellLines = cells.map((cell, colIndex) => {
146
+ const text = cellAnsiText(cell);
147
+ return wrapText(text, columnWidths[colIndex]!, {hard: needsHardWrap});
148
+ });
149
+ const maxLines = Math.max(...cellLines.map((ls) => ls.length), 1);
150
+ const verticalOffsets = cellLines.map((ls) => Math.floor((maxLines - ls.length) / 2));
151
+ const result: string[] = [];
152
+ for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
153
+ let line = '│';
154
+ for (let colIndex = 0; colIndex < cells.length; colIndex++) {
155
+ const ls = cellLines[colIndex]!;
156
+ const offset = verticalOffsets[colIndex]!;
157
+ const cIdx = lineIdx - offset;
158
+ const lineText = cIdx >= 0 && cIdx < ls.length ? ls[cIdx]! : '';
159
+ const width = columnWidths[colIndex]!;
160
+ const align = isHeader ? 'center' : (token.align?.[colIndex] as 'left' | 'center' | 'right' | undefined) ?? 'left';
161
+ const displayW = stringWidth(stripAnsi(lineText));
162
+ line += ' ' + padAligned(lineText, displayW, width, align) + ' │';
163
+ }
164
+ result.push(line);
165
+ }
166
+ return result;
167
+ }
168
+
169
+ function renderBorderLine(type: 'top' | 'middle' | 'bottom'): string {
170
+ const [left, mid, cross, right] = {
171
+ top: ['┌', '─', '┬', '┐'],
172
+ middle: ['├', '─', '┼', '┤'],
173
+ bottom: ['└', '─', '┴', '┘'],
174
+ }[type] as [string, string, string, string];
175
+ let line = left;
176
+ columnWidths.forEach((width, colIndex) => {
177
+ line += mid.repeat(width + 2);
178
+ line += colIndex < columnWidths.length - 1 ? cross : right;
179
+ });
180
+ return line;
181
+ }
182
+
183
+ function calculateMaxRowLines(): number {
184
+ let maxLines = 1;
185
+ for (let i = 0; i < headerCells.length; i++) {
186
+ const wrapped = wrapText(cellAnsiText(headerCells[i]), columnWidths[i]!, {hard: needsHardWrap});
187
+ maxLines = Math.max(maxLines, wrapped.length);
188
+ }
189
+ for (const row of rowsCells) {
190
+ for (let i = 0; i < row.length; i++) {
191
+ const wrapped = wrapText(cellAnsiText(row[i]), columnWidths[i]!, {hard: needsHardWrap});
192
+ maxLines = Math.max(maxLines, wrapped.length);
193
+ }
194
+ }
195
+ return maxLines;
196
+ }
197
+
198
+ const maxRowLines = calculateMaxRowLines();
199
+ const useVerticalFormat = maxRowLines > MAX_ROW_LINES;
200
+
201
+ function renderVerticalFormat(): string {
202
+ const lines: string[] = [];
203
+ const headers = headerCells.map((h) => cellAnsiText(h));
204
+ const separator = '─'.repeat(Math.min(terminalWidth - 1, 40));
205
+ rowsCells.forEach((row, rowIndex) => {
206
+ if (rowIndex > 0) lines.push(separator);
207
+ row.forEach((cell, colIndex) => {
208
+ const label = headers[colIndex] ?? `Column ${colIndex + 1}`;
209
+ const value = cellPlainText(cell).replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
210
+ const firstLineWidth = Math.max(terminalWidth - stringWidth(label) - 3, 10);
211
+ const wrappedValue = wrapText(value, firstLineWidth);
212
+ lines.push(`${label}: ${wrappedValue[0] ?? ''}`);
213
+ for (let i = 1; i < wrappedValue.length; i++) {
214
+ const l = wrappedValue[i]!;
215
+ if (!l.trim()) return;
216
+ lines.push(` ${l}`);
217
+ }
218
+ });
219
+ });
220
+ return lines.join('\n');
221
+ }
222
+
223
+ if (useVerticalFormat) {
224
+ return <Text>{renderVerticalFormat()}</Text>;
225
+ }
226
+
227
+ const tableLines: string[] = [];
228
+ tableLines.push(renderBorderLine('top'));
229
+ tableLines.push(...renderRowLines(headerCells, true));
230
+ tableLines.push(renderBorderLine('middle'));
231
+ rowsCells.forEach((row, rowIndex) => {
232
+ tableLines.push(...renderRowLines(row, false));
233
+ if (rowIndex < rowsCells.length - 1) {
234
+ tableLines.push(renderBorderLine('middle'));
235
+ }
236
+ });
237
+ tableLines.push(renderBorderLine('bottom'));
238
+
239
+ const maxLineWidth = Math.max(...tableLines.map((l) => stringWidth(stripAnsi(l))));
240
+ if (maxLineWidth > terminalWidth - SAFETY_MARGIN) {
241
+ return <Text>{renderVerticalFormat()}</Text>;
242
+ }
243
+
244
+ return <Text>{tableLines.join('\n')}</Text>;
245
+ }
@@ -0,0 +1,425 @@
1
+ import React, {useEffect, useMemo, useState} from 'react';
2
+ import {Box, Text, useInput} from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+
5
+ import type {UiLanguage} from '../i18n.js';
6
+ import {t} from '../i18n.js';
7
+ import {useTheme} from '../theme/ThemeContext.js';
8
+
9
+ type QuestionOption = {
10
+ label: string;
11
+ description?: string;
12
+ preview?: string;
13
+ };
14
+
15
+ type QuestionItem = {
16
+ question: string;
17
+ header?: string;
18
+ options?: QuestionOption[];
19
+ multiSelect?: boolean;
20
+ };
21
+
22
+ function QuestionModal({
23
+ modal,
24
+ modalInput,
25
+ setModalInput,
26
+ onSubmit,
27
+ language,
28
+ }: {
29
+ modal: Record<string, unknown>;
30
+ modalInput: string;
31
+ setModalInput: (value: string) => void;
32
+ onSubmit: (value: string) => void;
33
+ language: UiLanguage;
34
+ }): React.JSX.Element {
35
+ const theme = useTheme();
36
+ const [extraLines, setExtraLines] = useState<string[]>([]);
37
+ const [optionIndex, setOptionIndex] = useState(0);
38
+ const [isCustomInput, setIsCustomInput] = useState(false);
39
+ const [selectedIndices, setSelectedIndices] = useState<Set<number>>(new Set());
40
+
41
+ const questions: QuestionItem[] = useMemo(() => {
42
+ const raw = modal.questions;
43
+ if (!Array.isArray(raw)) return [];
44
+ return raw as QuestionItem[];
45
+ }, [modal.questions]);
46
+
47
+ const firstQuestion = questions.length > 0 ? questions[0] : null;
48
+ const options = firstQuestion?.options ?? [];
49
+ const hasOptions = options.length > 0;
50
+ const isMultiSelect = firstQuestion?.multiSelect === true && hasOptions;
51
+
52
+ type OptionEntry = {type: 'option'; label: string; description?: string} | {type: 'other'; label: string; description?: undefined};
53
+
54
+ const allOptions = useMemo(() => {
55
+ if (!hasOptions) return [] as OptionEntry[];
56
+ const result: OptionEntry[] = options.map((opt) => ({type: 'option' as const, label: opt.label, description: opt.description}));
57
+ // 多选模式下不追加"其他"选项
58
+ if (isMultiSelect) return result;
59
+ const hasOtherAlready = options.some((opt) => {
60
+ const lbl = opt.label.toLowerCase();
61
+ return lbl === 'other' || lbl === '其他' || lbl.startsWith('other') || lbl.startsWith('其他');
62
+ });
63
+ if (!hasOtherAlready) {
64
+ result.push({type: 'other' as const, label: language === 'zh-CN' ? '其他(手动输入)' : 'Other (type your answer)', description: undefined});
65
+ }
66
+ return result;
67
+ }, [options, hasOptions, isMultiSelect, language]);
68
+
69
+ useEffect(() => {
70
+ setOptionIndex(0);
71
+ setIsCustomInput(false);
72
+ setSelectedIndices(new Set());
73
+ }, [hasOptions, allOptions.length, isMultiSelect]);
74
+
75
+ useInput((_chunk, key) => {
76
+ // ---- 自定义输入模式 ----
77
+ if (isCustomInput) {
78
+ if (key.shift && key.return) {
79
+ setExtraLines((lines) => [...lines, modalInput]);
80
+ setModalInput('');
81
+ }
82
+ if (key.escape) {
83
+ setIsCustomInput(false);
84
+ setModalInput('');
85
+ }
86
+ return;
87
+ }
88
+
89
+ // ---- 多选模式 ----
90
+ if (isMultiSelect && allOptions.length > 0) {
91
+ if (key.upArrow) {
92
+ setOptionIndex((i) => Math.max(0, i - 1));
93
+ return;
94
+ }
95
+ if (key.downArrow) {
96
+ setOptionIndex((i) => Math.min(allOptions.length - 1, i + 1));
97
+ return;
98
+ }
99
+ if (key.return) {
100
+ // 收集所有选中项
101
+ const selected = allOptions
102
+ .filter((_, i) => selectedIndices.has(i))
103
+ .map((opt) => opt.label);
104
+ // 如果什么都没选,默认选中当前高亮的选项
105
+ if (selected.length === 0) {
106
+ selected.push(allOptions[optionIndex]!.label);
107
+ }
108
+ const header = firstQuestion?.header ?? 'answer';
109
+ onSubmit(JSON.stringify({[header]: selected}));
110
+ return;
111
+ }
112
+ if (_chunk === ' ') {
113
+ // Space 切换选中/取消
114
+ setSelectedIndices((prev) => {
115
+ const next = new Set(prev);
116
+ if (next.has(optionIndex)) {
117
+ next.delete(optionIndex);
118
+ } else {
119
+ next.add(optionIndex);
120
+ }
121
+ return next;
122
+ });
123
+ return;
124
+ }
125
+ // 数字键也切换选中(不立即提交)
126
+ const num = parseInt(_chunk, 10);
127
+ if (num >= 1 && num <= allOptions.length) {
128
+ setSelectedIndices((prev) => {
129
+ const next = new Set(prev);
130
+ const idx = num - 1;
131
+ if (next.has(idx)) {
132
+ next.delete(idx);
133
+ } else {
134
+ next.add(idx);
135
+ }
136
+ return next;
137
+ });
138
+ return;
139
+ }
140
+ return;
141
+ }
142
+
143
+ // ---- 单选模式(原逻辑) ----
144
+ if (hasOptions && allOptions.length > 0) {
145
+ if (key.upArrow) {
146
+ setOptionIndex((i) => Math.max(0, i - 1));
147
+ return;
148
+ }
149
+ if (key.downArrow) {
150
+ setOptionIndex((i) => Math.min(allOptions.length - 1, i + 1));
151
+ return;
152
+ }
153
+ if (key.return) {
154
+ const selected = allOptions[optionIndex];
155
+ if (selected?.type === 'other') {
156
+ setIsCustomInput(true);
157
+ } else if (selected) {
158
+ const idx = optionIndex + 1;
159
+ onSubmit(`${idx}. ${selected.label}`);
160
+ }
161
+ return;
162
+ }
163
+ const num = parseInt(_chunk, 10);
164
+ if (num >= 1 && num <= allOptions.length) {
165
+ const target = allOptions[num - 1];
166
+ if (target?.type === 'other') {
167
+ setIsCustomInput(true);
168
+ } else if (target) {
169
+ onSubmit(`${num}. ${target.label}`);
170
+ }
171
+ return;
172
+ }
173
+ } else {
174
+ if (key.shift && key.return) {
175
+ setExtraLines((lines) => [...lines, modalInput]);
176
+ setModalInput('');
177
+ }
178
+ }
179
+ });
180
+
181
+ const handleSubmit = (value: string): void => {
182
+ if (isCustomInput) {
183
+ const allLines = [...extraLines, value];
184
+ setExtraLines([]);
185
+ setIsCustomInput(false);
186
+ onSubmit(allLines.join('\n'));
187
+ return;
188
+ }
189
+ if (hasOptions) {
190
+ return;
191
+ }
192
+ const allLines = [...extraLines, value];
193
+ setExtraLines([]);
194
+ onSubmit(allLines.join('\n'));
195
+ };
196
+
197
+ const toolName = modal.tool_name ? String(modal.tool_name) : null;
198
+ const reason = modal.reason ? String(modal.reason) : null;
199
+ const question = String(modal.question ?? 'Question');
200
+ const header = firstQuestion?.header;
201
+
202
+ return (
203
+ <Box flexDirection="column" marginTop={1}>
204
+ <Box>
205
+ <Text color={theme.colors.illusion}>{theme.icons.pointer} </Text>
206
+ {header ? (
207
+ <>
208
+ <Text color={theme.colors.suggestion} bold>[{header}] </Text>
209
+ <Text bold>{firstQuestion?.question ?? question}</Text>
210
+ </>
211
+ ) : (
212
+ <Text bold>{firstQuestion?.question ?? question}</Text>
213
+ )}
214
+ </Box>
215
+ {toolName ? (
216
+ <Box>
217
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
218
+ <Text dimColor>Tool: </Text>
219
+ <Text color={theme.colors.info}>{toolName}</Text>
220
+ </Box>
221
+ ) : null}
222
+ {reason ? (
223
+ <Box>
224
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
225
+ <Text dimColor>{reason}</Text>
226
+ </Box>
227
+ ) : null}
228
+
229
+ {hasOptions && !isCustomInput ? (
230
+ <Box flexDirection="column" marginTop={1}>
231
+ {allOptions.map((opt, i) => {
232
+ const isCurrent = i === optionIndex;
233
+ const isSelected = isMultiSelect ? selectedIndices.has(i) : false;
234
+ return (
235
+ <Box key={i}>
236
+ <Text color={isCurrent ? theme.colors.suggestion : theme.colors.muted}>
237
+ {isCurrent ? `${theme.icons.pointer} ` : ' '}
238
+ </Text>
239
+ {isMultiSelect && (
240
+ <Text color={isSelected ? theme.colors.suggestion : theme.colors.muted}>
241
+ [{isSelected ? 'x' : ' '}]
242
+ </Text>
243
+ )}
244
+ <Text color={isCurrent && !isMultiSelect ? theme.colors.suggestion : (isMultiSelect && isSelected ? theme.colors.suggestion : undefined)} bold={isCurrent && !isMultiSelect} dimColor={!isCurrent}>
245
+ {`${i + 1}. `}
246
+ {opt.label}
247
+ </Text>
248
+ {opt.description ? (
249
+ <Box marginLeft={1}>
250
+ <Text dimColor>{theme.icons.middleDot} {opt.description}</Text>
251
+ </Box>
252
+ ) : null}
253
+ {isCurrent && !isMultiSelect ? <Text dimColor>{' [enter]'}</Text> : null}
254
+ </Box>
255
+ );
256
+ })}
257
+ <Box marginTop={0}>
258
+ <Text dimColor>
259
+ <Text color={theme.colors.muted}>↑↓</Text> navigate
260
+ {isMultiSelect ? (
261
+ <>
262
+ <Text> {theme.icons.middleDot} </Text>
263
+ <Text color={theme.colors.muted}>Space</Text> toggle
264
+ <Text> {theme.icons.middleDot} </Text>
265
+ <Text color={theme.colors.muted}>1-{allOptions.length}</Text> toggle
266
+ <Text> {theme.icons.middleDot} </Text>
267
+ <Text color={theme.colors.muted}>↵</Text> confirm
268
+ </>
269
+ ) : (
270
+ <>
271
+ <Text> {theme.icons.middleDot} </Text>
272
+ <Text color={theme.colors.muted}>↵</Text> select
273
+ <Text> {theme.icons.middleDot} </Text>
274
+ <Text color={theme.colors.muted}>1-{allOptions.length}</Text> quick
275
+ </>
276
+ )}
277
+ </Text>
278
+ </Box>
279
+ </Box>
280
+ ) : null}
281
+
282
+ {(isCustomInput || !hasOptions) ? (
283
+ <>
284
+ {isCustomInput ? (
285
+ <Box marginTop={1}>
286
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
287
+ <Text dimColor>{language === 'zh-CN' ? '请输入您的回答:' : 'Type your answer:'}</Text>
288
+ </Box>
289
+ ) : null}
290
+ {extraLines.length > 0 && (
291
+ <Box flexDirection="column" marginTop={1}>
292
+ {extraLines.map((line, i) => (
293
+ <Box key={i}>
294
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
295
+ <Text dimColor>{line}</Text>
296
+ </Box>
297
+ ))}
298
+ </Box>
299
+ )}
300
+ <Box marginTop={1}>
301
+ <Text color={theme.colors.illusion}>{theme.icons.pointer} </Text>
302
+ <TextInput value={modalInput} onChange={setModalInput} onSubmit={handleSubmit} />
303
+ </Box>
304
+ </>
305
+ ) : null}
306
+ </Box>
307
+ );
308
+ }
309
+
310
+ function PermissionModal({
311
+ modal,
312
+ }: {
313
+ modal: Record<string, unknown>;
314
+ }): React.JSX.Element {
315
+ const theme = useTheme();
316
+ const toolName = String(modal.tool_name ?? 'tool');
317
+ const reason = modal.reason ? String(modal.reason) : null;
318
+
319
+ return (
320
+ <Box flexDirection="column" marginTop={1}>
321
+ <Box>
322
+ <Text color={theme.colors.warning}>{theme.icons.pointer} </Text>
323
+ <Text bold>Allow </Text>
324
+ <Text color={theme.colors.info} bold>{toolName}</Text>
325
+ <Text bold>?</Text>
326
+ </Box>
327
+ {reason ? (
328
+ <Box>
329
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
330
+ <Text dimColor>{reason}</Text>
331
+ </Box>
332
+ ) : null}
333
+ <Box>
334
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
335
+ <Text dimColor>
336
+ <Text color={theme.colors.muted}>↑↓</Text> navigate
337
+ <Text> {theme.icons.middleDot} </Text>
338
+ <Text color={theme.colors.muted}>↵</Text> select
339
+ </Text>
340
+ </Box>
341
+ </Box>
342
+ );
343
+ }
344
+
345
+ function McpAuthModal({
346
+ modal,
347
+ modalInput,
348
+ setModalInput,
349
+ onSubmit,
350
+ language,
351
+ }: {
352
+ modal: Record<string, unknown>;
353
+ modalInput: string;
354
+ setModalInput: (value: string) => void;
355
+ onSubmit: (value: string) => void;
356
+ language: UiLanguage;
357
+ }): React.JSX.Element {
358
+ const theme = useTheme();
359
+ const prompt = String(modal.prompt ?? 'Provide auth details');
360
+
361
+ return (
362
+ <Box flexDirection="column" marginTop={1}>
363
+ <Box>
364
+ <Text color={theme.colors.warning}>{theme.icons.pointer} </Text>
365
+ <Text bold>MCP Authentication</Text>
366
+ </Box>
367
+ <Box>
368
+ <Text dimColor>{` ${theme.icons.resultPrefix} `}</Text>
369
+ <Text dimColor>{prompt}</Text>
370
+ </Box>
371
+ <Box marginTop={1}>
372
+ <Text color={theme.colors.illusion}>{theme.icons.pointer} </Text>
373
+ <TextInput value={modalInput} onChange={setModalInput} onSubmit={onSubmit} />
374
+ </Box>
375
+ </Box>
376
+ );
377
+ }
378
+
379
+ export function ModalHost({
380
+ modal,
381
+ modalInput,
382
+ setModalInput,
383
+ onSubmit,
384
+ language,
385
+ }: {
386
+ modal: Record<string, unknown> | null;
387
+ modalInput: string;
388
+ setModalInput: (value: string) => void;
389
+ onSubmit: (value: string) => void;
390
+ language: UiLanguage;
391
+ }): React.JSX.Element | null {
392
+ if (!modal) {
393
+ return null;
394
+ }
395
+
396
+ if (modal.kind === 'permission') {
397
+ return <PermissionModal modal={modal} />;
398
+ }
399
+
400
+ if (modal.kind === 'question') {
401
+ return (
402
+ <QuestionModal
403
+ modal={modal}
404
+ modalInput={modalInput}
405
+ setModalInput={setModalInput}
406
+ onSubmit={onSubmit}
407
+ language={language}
408
+ />
409
+ );
410
+ }
411
+
412
+ if (modal.kind === 'mcp_auth') {
413
+ return (
414
+ <McpAuthModal
415
+ modal={modal}
416
+ modalInput={modalInput}
417
+ setModalInput={setModalInput}
418
+ onSubmit={onSubmit}
419
+ language={language}
420
+ />
421
+ );
422
+ }
423
+
424
+ return null;
425
+ }