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,142 @@
1
+ import React, {useEffect, useState} from 'react';
2
+ import {Box, Text} from 'ink';
3
+
4
+ import {useTheme} from '../theme/ThemeContext.js';
5
+ import type {TaskSnapshot} from '../types.js';
6
+
7
+ const SEP = ' · ';
8
+
9
+ function AutoModeIndicator(): React.JSX.Element {
10
+ const theme = useTheme();
11
+ return (
12
+ <Box marginLeft={1}>
13
+ <Text backgroundColor={theme.colors.illusion} color={theme.colors.background} bold>
14
+ {' AUTO '}
15
+ </Text>
16
+ </Box>
17
+ );
18
+ }
19
+
20
+ function TokenDisplay({
21
+ inputTokens,
22
+ outputTokens,
23
+ color,
24
+ }: {
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ color: string;
28
+ }): React.JSX.Element {
29
+ return (
30
+ <Text color={color}>
31
+ <Text dimColor>{formatNum(inputTokens)}</Text>
32
+ <Text dimColor>↓</Text>
33
+ <Text> </Text>
34
+ <Text dimColor>{formatNum(outputTokens)}</Text>
35
+ <Text dimColor>↑</Text>
36
+ </Text>
37
+ );
38
+ }
39
+
40
+ function TaskIndicator({count}: {count: number}): React.JSX.Element {
41
+ const theme = useTheme();
42
+ return (
43
+ <Box>
44
+ <Text color={theme.colors.illusion}>{theme.icons.inProgress}</Text>
45
+ <Text color={theme.colors.illusion}> {count} task{count !== 1 ? 's' : ''}</Text>
46
+ </Box>
47
+ );
48
+ }
49
+
50
+ function McpIndicator({count}: {count: number}): React.JSX.Element {
51
+ const theme = useTheme();
52
+ return (
53
+ <Box>
54
+ <Text color={theme.colors.illusion}> · {count} MCP</Text>
55
+ </Box>
56
+ );
57
+ }
58
+
59
+ function AgentIndicator({count}: {count: number}): React.JSX.Element {
60
+ const theme = useTheme();
61
+ const [visible, setVisible] = useState(true);
62
+
63
+ useEffect(() => {
64
+ const interval = setInterval(() => setVisible(v => !v), 500);
65
+ return () => clearInterval(interval);
66
+ }, []);
67
+
68
+ if (!visible) {
69
+ return <Box><Text> </Text></Box>;
70
+ }
71
+
72
+ return (
73
+ <Box>
74
+ <Text color={theme.colors.illusion}> · {count} agent{count !== 1 ? 's' : ''}</Text>
75
+ </Box>
76
+ );
77
+ }
78
+
79
+ export function StatusBar({
80
+ status,
81
+ tasks,
82
+ }: {
83
+ status: Record<string, unknown>;
84
+ tasks: TaskSnapshot[];
85
+ }): React.JSX.Element {
86
+ const theme = useTheme();
87
+ const model = String(status.model ?? 'unknown');
88
+ const mode = String(status.permission_mode ?? 'default');
89
+ const taskCount = tasks.filter(
90
+ (task) => task.status === 'pending' || task.status === 'in_progress'
91
+ ).length;
92
+ const mcpCount = Number(status.mcp_connected ?? 0);
93
+ const agentCount = Number(status.agent_count ?? 0);
94
+ const inputTokens = Number(status.input_tokens ?? 0);
95
+ const outputTokens = Number(status.output_tokens ?? 0);
96
+ const isAutoMode = mode === 'full_auto' || mode === 'auto';
97
+
98
+ return (
99
+ <Box flexDirection="column" marginTop={1}>
100
+ <Box flexDirection="row">
101
+ <Text color={theme.colors.text}>{'─'.repeat(60)}</Text>
102
+ </Box>
103
+ <Box flexDirection="row" alignItems="center">
104
+ <Text color={theme.colors.illusion}>{model}</Text>
105
+ {(inputTokens > 0 || outputTokens > 0) ? (
106
+ <>
107
+ <Text color={theme.colors.illusion}>{SEP}</Text>
108
+ <TokenDisplay inputTokens={inputTokens} outputTokens={outputTokens} color={theme.colors.illusion} />
109
+ </>
110
+ ) : null}
111
+ {mode !== 'default' ? (
112
+ <>
113
+ <Text color={theme.colors.illusion}>{SEP}</Text>
114
+ <Text color={theme.colors.illusion}>{mode}</Text>
115
+ </>
116
+ ) : null}
117
+ {taskCount > 0 ? (
118
+ <>
119
+ <Text color={theme.colors.illusion}>{SEP}</Text>
120
+ <TaskIndicator count={taskCount} />
121
+ </>
122
+ ) : null}
123
+ {mcpCount > 0 ? (
124
+ <McpIndicator count={mcpCount} />
125
+ ) : null}
126
+ {agentCount > 0 ? <AgentIndicator count={agentCount} /> : null}
127
+ <Box flexGrow={1} />
128
+ {isAutoMode ? <AutoModeIndicator /> : null}
129
+ </Box>
130
+ </Box>
131
+ );
132
+ }
133
+
134
+ function formatNum(n: number): string {
135
+ if (n >= 1000000) {
136
+ return `${(n / 1000000).toFixed(1)}M`;
137
+ }
138
+ if (n >= 1000) {
139
+ return `${(n / 1000).toFixed(1)}k`;
140
+ }
141
+ return String(n);
142
+ }
@@ -0,0 +1,141 @@
1
+ import React, {useState} from 'react';
2
+ import {Box, Text, useInput} from 'ink';
3
+
4
+ import type {ThemeConfig} from '../theme/ThemeContext.js';
5
+ import {useTheme} from '../theme/ThemeContext.js';
6
+ export type SwarmTeammate = {
7
+ name: string;
8
+ status: 'running' | 'idle' | 'done' | 'error';
9
+ duration?: number; // seconds
10
+ task?: string;
11
+ };
12
+
13
+ export type SwarmNotification = {
14
+ from: string;
15
+ message: string;
16
+ timestamp: number;
17
+ };
18
+
19
+ function statusIcon(status: SwarmTeammate['status'], theme: ThemeConfig): string {
20
+ switch (status) {
21
+ case 'running':
22
+ return theme.icons.inProgress;
23
+ case 'idle':
24
+ return theme.icons.pending;
25
+ case 'done':
26
+ return theme.icons.completed;
27
+ case 'error':
28
+ return theme.icons.error;
29
+ }
30
+ }
31
+
32
+ function statusColor(status: SwarmTeammate['status']): string {
33
+ switch (status) {
34
+ case 'running':
35
+ return 'green';
36
+ case 'idle':
37
+ return 'yellow';
38
+ case 'done':
39
+ return 'cyan';
40
+ case 'error':
41
+ return 'red';
42
+ }
43
+ }
44
+
45
+ function formatDuration(seconds: number): string {
46
+ if (seconds < 60) {
47
+ return `${seconds}s`;
48
+ }
49
+ const m = Math.floor(seconds / 60);
50
+ const s = seconds % 60;
51
+ return `${m}m${s}s`;
52
+ }
53
+
54
+ export function SwarmPanel({
55
+ teammates,
56
+ notifications,
57
+ collapsed: initialCollapsed = false,
58
+ }: {
59
+ teammates: SwarmTeammate[];
60
+ notifications: SwarmNotification[];
61
+ collapsed?: boolean;
62
+ }): React.JSX.Element | null {
63
+ const theme = useTheme();
64
+ const [collapsed, setCollapsed] = useState(initialCollapsed);
65
+
66
+ useInput((chunk, key) => {
67
+ if (key.ctrl && chunk === 'w') {
68
+ setCollapsed((c) => !c);
69
+ }
70
+ });
71
+
72
+ if (teammates.length === 0 && notifications.length === 0) {
73
+ return null;
74
+ }
75
+
76
+ const activeCount = teammates.filter((t) => t.status === 'running').length;
77
+
78
+ if (collapsed) {
79
+ return (
80
+ <Box>
81
+ <Text color={theme.colors.accent} bold>
82
+ {theme.icons.inProgress}{' '}
83
+ </Text>
84
+ <Text dimColor>
85
+ Swarm: {teammates.length} agents ({activeCount} active)
86
+ </Text>
87
+ <Text dimColor> [ctrl+w to expand]</Text>
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.accent} paddingX={1} marginTop={1}>
94
+ <Box>
95
+ <Text color={theme.colors.accent} bold>
96
+ {theme.icons.inProgress}{' '}
97
+ </Text>
98
+ <Text bold>Swarm</Text>
99
+ <Text dimColor>
100
+ {' '}
101
+ ({activeCount}/{teammates.length} active) [ctrl+w to collapse]
102
+ </Text>
103
+ </Box>
104
+
105
+ {teammates.length > 0 && (
106
+ <Box flexDirection="column" marginTop={1}>
107
+ {teammates.map((teammate) => (
108
+ <Box key={teammate.name} flexDirection="row" marginBottom={0}>
109
+ <Text color={statusColor(teammate.status)}>{statusIcon(teammate.status, theme)} </Text>
110
+ <Box flexDirection="column">
111
+ <Box>
112
+ <Text bold color={statusColor(teammate.status)}>
113
+ {teammate.name}
114
+ </Text>
115
+ {teammate.duration !== undefined && (
116
+ <Text dimColor> ({formatDuration(teammate.duration)})</Text>
117
+ )}
118
+ </Box>
119
+ {teammate.task && (
120
+ <Text dimColor> {teammate.task.slice(0, 60)}{teammate.task.length > 60 ? '…' : ''}</Text>
121
+ )}
122
+ </Box>
123
+ </Box>
124
+ ))}
125
+ </Box>
126
+ )}
127
+
128
+ {notifications.length > 0 && (
129
+ <Box flexDirection="column" marginTop={1}>
130
+ <Text dimColor bold>Recent notifications:</Text>
131
+ {notifications.slice(-3).map((n, i) => (
132
+ <Box key={i}>
133
+ <Text dimColor>[{n.from}] </Text>
134
+ <Text>{n.message.slice(0, 70)}{n.message.length > 70 ? '…' : ''}</Text>
135
+ </Box>
136
+ ))}
137
+ </Box>
138
+ )}
139
+ </Box>
140
+ );
141
+ }
@@ -0,0 +1,126 @@
1
+ import React, {useEffect, useRef, useState} 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 {TodoItemSnapshot} from '../types.js';
7
+
8
+ /** 所有任务完成后自动隐藏的延迟时间(毫秒) */
9
+ const HIDE_DELAY_MS = 5000;
10
+
11
+ /** 最近完成任务的高亮时间(毫秒) */
12
+ const RECENT_COMPLETED_TTL_MS = 30_000;
13
+
14
+ export function TodoPanel({items}: {items: TodoItemSnapshot[]}): React.JSX.Element {
15
+ const theme = useTheme();
16
+ const [hidden, setHidden] = useState(false);
17
+ const hideTimerRef = useRef<NodeJS.Timeout | null>(null);
18
+ const completionTimeRef = useRef<Record<string, number>>({});
19
+
20
+ if (items.length === 0 || hidden) {
21
+ return <></>;
22
+ }
23
+
24
+ const completed = items.filter((t) => t.status === 'completed').length;
25
+ const inProgress = items.filter((t) => t.status === 'in_progress').length;
26
+ const pending = items.filter((t) => t.status === 'pending').length;
27
+ const allDone = completed === items.length && items.length > 0;
28
+
29
+ // 跟踪任务完成时间
30
+ const now = Date.now();
31
+ for (const item of items) {
32
+ if (item.status === 'completed' && !(item.content in completionTimeRef.current)) {
33
+ completionTimeRef.current[item.content] = now;
34
+ }
35
+ }
36
+
37
+ // 所有任务完成后延迟隐藏
38
+ useEffect(() => {
39
+ if (allDone) {
40
+ if (hideTimerRef.current === null) {
41
+ hideTimerRef.current = setTimeout(() => {
42
+ setHidden(true);
43
+ hideTimerRef.current = null;
44
+ }, HIDE_DELAY_MS);
45
+ }
46
+ } else {
47
+ setHidden(false);
48
+ if (hideTimerRef.current !== null) {
49
+ clearTimeout(hideTimerRef.current);
50
+ hideTimerRef.current = null;
51
+ }
52
+ }
53
+ return () => {
54
+ if (hideTimerRef.current !== null) {
55
+ clearTimeout(hideTimerRef.current);
56
+ hideTimerRef.current = null;
57
+ }
58
+ };
59
+ }, [allDone]);
60
+
61
+ // 排序:最近完成 > in_progress > pending > 较早完成
62
+ const sorted = [...items].sort((a, b) => {
63
+ const aRecent = a.status === 'completed' && (now - (completionTimeRef.current[a.content] ?? 0)) < RECENT_COMPLETED_TTL_MS;
64
+ const bRecent = b.status === 'completed' && (now - (completionTimeRef.current[b.content] ?? 0)) < RECENT_COMPLETED_TTL_MS;
65
+ const order = (item: TodoItemSnapshot, isRecent: boolean): number => {
66
+ if (item.status === 'in_progress') return 0;
67
+ if (isRecent) return 1;
68
+ if (item.status === 'pending') return 2;
69
+ return 3;
70
+ };
71
+ return order(a, aRecent) - order(b, bRecent);
72
+ });
73
+
74
+ return (
75
+ <Box flexDirection="column" marginTop={1}>
76
+ <Box marginBottom={0}>
77
+ <Text color={theme.colors.illusion} bold>{theme.icons.pointer} </Text>
78
+ <Text bold>Todos</Text>
79
+ <Text dimColor>{` ${completed}/${items.length} done`}</Text>
80
+ {inProgress > 0 ? <Text color={theme.colors.info}>{` ${theme.icons.middleDot} ${inProgress} active`}</Text> : null}
81
+ {pending > 0 ? <Text dimColor>{` ${theme.icons.middleDot} ${pending} open`}</Text> : null}
82
+ </Box>
83
+ {sorted.map((item, i) => (
84
+ <TodoRow key={i} item={item} theme={theme} now={now} completionTimes={completionTimeRef.current} />
85
+ ))}
86
+ </Box>
87
+ );
88
+ }
89
+
90
+ function TodoRow({item, theme, now, completionTimes}: {item: TodoItemSnapshot; theme: ThemeConfig; now: number; completionTimes: Record<string, number>}): React.JSX.Element {
91
+ let icon: string;
92
+ let color: string;
93
+
94
+ switch (item.status) {
95
+ case 'completed':
96
+ icon = theme.icons.check;
97
+ color = theme.colors.success;
98
+ break;
99
+ case 'in_progress':
100
+ icon = theme.icons.inProgress;
101
+ color = theme.colors.illusion;
102
+ break;
103
+ default:
104
+ icon = theme.icons.pending;
105
+ color = theme.colors.muted;
106
+ break;
107
+ }
108
+
109
+ const isCompleted = item.status === 'completed';
110
+ // 最近完成的任务不高亮(不dim)
111
+ const isRecentCompleted = isCompleted && (now - (completionTimes[item.content] ?? 0)) < RECENT_COMPLETED_TTL_MS;
112
+
113
+ return (
114
+ <Box>
115
+ <Text color={color}>{icon} </Text>
116
+ <Text
117
+ color={isCompleted && !isRecentCompleted ? theme.colors.muted : undefined}
118
+ dimColor={isCompleted && !isRecentCompleted}
119
+ strikethrough={isCompleted}
120
+ bold={item.status === 'in_progress'}
121
+ >
122
+ {item.content}
123
+ </Text>
124
+ </Box>
125
+ );
126
+ }
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+ import {Box, Text} from 'ink';
3
+
4
+ import type {UiLanguage} from '../i18n.js';
5
+ import type {ThemeConfig} from '../theme/ThemeContext.js';
6
+ import {useTheme} from '../theme/ThemeContext.js';
7
+ import type {TranscriptItem} from '../types.js';
8
+ import {useTerminalSize} from '../hooks/useTerminalSize.js';
9
+ import {stringWidth} from '../utils/markdown.js';
10
+
11
+ const MAX_OUTPUT_LINES = 8;
12
+ const MAX_COMMAND_LINES = 2;
13
+ const MAX_COMMAND_CHARS = 160;
14
+
15
+ export function ToolCallDisplay({item, language}: {item: TranscriptItem; language: UiLanguage}): React.JSX.Element {
16
+ const theme = useTheme();
17
+
18
+ if (item.role === 'tool') {
19
+ return <ToolUseMessage item={item} theme={theme} />;
20
+ }
21
+
22
+ if (item.role === 'tool_result') {
23
+ return <ToolResultMessage item={item} theme={theme} />;
24
+ }
25
+
26
+ return <Text>{item.text}</Text>;
27
+ }
28
+
29
+ function ToolUseMessage({
30
+ item,
31
+ theme,
32
+ }: {
33
+ item: TranscriptItem;
34
+ theme: ThemeConfig;
35
+ }): React.JSX.Element {
36
+ const toolName = item.tool_name ?? 'tool';
37
+ const summary = summarizeInput(toolName, item.tool_input, item.text);
38
+
39
+ return (
40
+ <Box>
41
+ <Text color={theme.colors.info}>{theme.icons.tool} </Text>
42
+ <Text color={theme.colors.info} bold>{toolName}</Text>
43
+ {summary ? (
44
+ <>
45
+ <Text dimColor>{' ('}</Text>
46
+ <Text dimColor>{summary}</Text>
47
+ <Text dimColor>{')'}</Text>
48
+ </>
49
+ ) : null}
50
+ </Box>
51
+ );
52
+ }
53
+
54
+ function ToolResultMessage({
55
+ item,
56
+ theme,
57
+ }: {
58
+ item: TranscriptItem;
59
+ theme: ThemeConfig;
60
+ }): React.JSX.Element {
61
+ const {columns: terminalWidth} = useTerminalSize();
62
+ const lines = item.text.split('\n');
63
+ const truncated = lines.length > MAX_OUTPUT_LINES;
64
+ const display = truncated
65
+ ? [...lines.slice(0, MAX_OUTPUT_LINES), `… +${lines.length - MAX_OUTPUT_LINES} lines`]
66
+ : lines;
67
+
68
+ const isError = item.is_error;
69
+ const icon = isError ? theme.icons.cross : theme.icons.check;
70
+ const iconColor = isError ? theme.colors.error : theme.colors.success;
71
+ // 可用宽度 = 终端宽度 - 前缀(2空格+图标) - 图标 - 安全边距
72
+ const prefixWidth = stringWidth(` ${theme.icons.resultPrefix} `) + stringWidth(`${icon} `);
73
+ const availableWidth = Math.max(20, terminalWidth - prefixWidth - 2);
74
+
75
+ return (
76
+ <Box flexDirection="column">
77
+ {display.map((line, i) => (
78
+ <Box key={i}>
79
+ <Text dimColor>{i === 0 ? ` ${theme.icons.resultPrefix} ` : ' '}</Text>
80
+ {i === 0 ? (
81
+ <Text color={iconColor}>{icon} </Text>
82
+ ) : null}
83
+ {i !== 0 ? <Text>{' '}</Text> : null}
84
+ <Text color={isError ? theme.colors.error : undefined} dimColor={!isError}>
85
+ {truncateToDisplayWidth(line, availableWidth)}
86
+ </Text>
87
+ </Box>
88
+ ))}
89
+ </Box>
90
+ );
91
+ }
92
+
93
+ function summarizeInput(toolName: string, toolInput?: Record<string, unknown>, fallback?: string): string {
94
+ if (!toolInput) {
95
+ return truncateCommand(fallback ?? '');
96
+ }
97
+
98
+ const lower = toolName.toLowerCase();
99
+
100
+ if ((lower === 'bash' || lower === 'powershell') && toolInput.command) {
101
+ return truncateCommand(String(toolInput.command));
102
+ }
103
+ if ((lower === 'read' || lower === 'fileread' || lower === 'read_file') && (toolInput.path || toolInput.file_path)) {
104
+ return String(toolInput.path ?? toolInput.file_path);
105
+ }
106
+ if ((lower === 'write' || lower === 'filewrite' || lower === 'write_file') && (toolInput.path || toolInput.file_path)) {
107
+ return String(toolInput.path ?? toolInput.file_path);
108
+ }
109
+ if ((lower === 'edit' || lower === 'fileedit' || lower === 'edit_file') && (toolInput.path || toolInput.file_path)) {
110
+ return String(toolInput.path ?? toolInput.file_path);
111
+ }
112
+ if (lower === 'grep' && toolInput.pattern) {
113
+ return `/${String(toolInput.pattern)}/`;
114
+ }
115
+ if (lower === 'glob' && toolInput.pattern) {
116
+ return String(toolInput.pattern);
117
+ }
118
+ if (lower === 'agent' && toolInput.description) {
119
+ return truncateCommand(String(toolInput.description));
120
+ }
121
+ if (lower === 'todowrite' || lower === 'todo_write') {
122
+ const todos = toolInput.todos;
123
+ if (Array.isArray(todos)) {
124
+ const total = todos.length;
125
+ const completed = todos.filter((t: {status: string}) => t.status === 'completed').length;
126
+ return `${completed}/${total} tasks`;
127
+ }
128
+ }
129
+ if (lower === 'ask_user_question') {
130
+ const questions = toolInput.questions;
131
+ if (Array.isArray(questions) && questions.length > 0) {
132
+ const q = questions[0] as Record<string, unknown>;
133
+ return truncateCommand(String(q.question ?? ''));
134
+ }
135
+ }
136
+
137
+ const entries = Object.entries(toolInput);
138
+ if (entries.length > 0) {
139
+ const [key, val] = entries[0];
140
+ return truncateCommand(`${key}=${String(val)}`);
141
+ }
142
+
143
+ return truncateCommand(fallback ?? '');
144
+ }
145
+
146
+ // 参考 claude-code 的截断策略:先按行截断,再按字符截断
147
+ function truncateCommand(str: string): string {
148
+ // 1. 按行分割
149
+ const lines = str.split('\n');
150
+
151
+ // 2. 移除每行首尾空格,过滤空行
152
+ const cleanedLines = lines.map(l => l.trim()).filter(l => l.length > 0);
153
+
154
+ // 3. 按行截断(最多 MAX_COMMAND_LINES 行)
155
+ const truncatedLines = cleanedLines.length > MAX_COMMAND_LINES
156
+ ? [...cleanedLines.slice(0, MAX_COMMAND_LINES)]
157
+ : cleanedLines;
158
+
159
+ // 4. 合并为单行
160
+ let result = truncatedLines.join(' ');
161
+
162
+ // 5. 按字符截断
163
+ const needsCharTruncation = result.length > MAX_COMMAND_CHARS || cleanedLines.length > MAX_COMMAND_LINES;
164
+ if (needsCharTruncation && result.length > MAX_COMMAND_CHARS) {
165
+ result = result.slice(0, MAX_COMMAND_CHARS);
166
+ // 优先在分号处截断(命令分隔符)
167
+ const lastSemicolon = result.lastIndexOf(';');
168
+ if (lastSemicolon > MAX_COMMAND_CHARS * 0.3) {
169
+ result = result.slice(0, lastSemicolon + 1);
170
+ } else {
171
+ // 其次在空格处截断
172
+ const lastSpace = result.lastIndexOf(' ');
173
+ if (lastSpace > MAX_COMMAND_CHARS * 0.5) {
174
+ result = result.slice(0, lastSpace);
175
+ }
176
+ }
177
+ }
178
+
179
+ // 6. 添加省略号
180
+ if (needsCharTruncation) {
181
+ result += '…';
182
+ }
183
+
184
+ return result;
185
+ }
186
+
187
+ function truncateToDisplayWidth(text: string, maxWidth: number): string {
188
+ if (stringWidth(text) <= maxWidth) {
189
+ return text;
190
+ }
191
+ let result = '';
192
+ let width = 0;
193
+ for (const ch of text) {
194
+ const charWidth = stringWidth(ch);
195
+ if (width + charWidth > Math.max(1, maxWidth - 1)) {
196
+ break;
197
+ }
198
+ result += ch;
199
+ width += charWidth;
200
+ }
201
+ return result + '…';
202
+ }
@@ -0,0 +1,79 @@
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 {TranscriptItem} from '../types.js';
7
+
8
+ const MAX_VISIBLE_ITEMS = 30;
9
+
10
+ export function TranscriptPane({
11
+ items,
12
+ assistantBuffer,
13
+ }: {
14
+ items: TranscriptItem[];
15
+ assistantBuffer: string;
16
+ }): React.JSX.Element {
17
+ const theme = useTheme();
18
+ const visible = items.slice(-MAX_VISIBLE_ITEMS);
19
+
20
+ return (
21
+ <Box flexDirection="column" width="68%" paddingRight={1}>
22
+ <Box marginBottom={1}>
23
+ <Text color={theme.colors.primary} bold>{theme.icons.chevron} Transcript</Text>
24
+ </Box>
25
+ <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.muted} paddingX={1} minHeight={24}>
26
+ {visible.map((item, index) => (
27
+ <Box key={`${index}-${item.role}`} flexDirection="row">
28
+ <Text color={roleColor(item.role, theme)} bold>
29
+ {labelFor(item.role, theme)}{' '}
30
+ </Text>
31
+ <Text color={roleColor(item.role, theme)}>{item.text}</Text>
32
+ </Box>
33
+ ))}
34
+ {assistantBuffer ? (
35
+ <Box flexDirection="row">
36
+ <Text color={theme.colors.success} bold>{theme.icons.assistant} </Text>
37
+ <Text color={theme.colors.success}>{assistantBuffer}</Text>
38
+ </Box>
39
+ ) : null}
40
+ </Box>
41
+ </Box>
42
+ );
43
+ }
44
+
45
+ function labelFor(role: TranscriptItem['role'], theme: ThemeConfig): string {
46
+ switch (role) {
47
+ case 'user':
48
+ return theme.icons.user;
49
+ case 'assistant':
50
+ return theme.icons.assistant;
51
+ case 'tool':
52
+ return theme.icons.tool;
53
+ case 'tool_result':
54
+ return theme.icons.check;
55
+ case 'system':
56
+ return theme.icons.system;
57
+ case 'log':
58
+ return theme.icons.bullet;
59
+ default:
60
+ return theme.icons.dot;
61
+ }
62
+ }
63
+
64
+ function roleColor(role: TranscriptItem['role'], theme: ThemeConfig): string {
65
+ switch (role) {
66
+ case 'assistant':
67
+ return theme.colors.success;
68
+ case 'tool':
69
+ return theme.colors.accent;
70
+ case 'tool_result':
71
+ return theme.colors.warning;
72
+ case 'system':
73
+ return theme.colors.info;
74
+ case 'log':
75
+ return theme.colors.muted;
76
+ default:
77
+ return theme.colors.text;
78
+ }
79
+ }