kiku-agent 1.3.0 → 1.3.2

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 (155) hide show
  1. package/README.md +7 -7
  2. package/package.json +4 -7
  3. package/server/agent/agentCore.js +2 -2
  4. package/server/agent/catalogEntityResolver.js +8 -0
  5. package/server/agent/cli.js +4 -0
  6. package/server/agent/eval/AGENTS.md +228 -0
  7. package/server/agent/eval/analyze.py +684 -0
  8. package/server/agent/eval/gates.json +15 -0
  9. package/server/agent/eval/modules/analysis/compare.js +198 -0
  10. package/server/agent/eval/modules/analysis/gates.js +72 -0
  11. package/server/agent/eval/modules/analysis/judge.js +70 -0
  12. package/server/agent/eval/modules/core/cli.js +453 -0
  13. package/server/agent/eval/modules/core/config.js +60 -0
  14. package/server/agent/eval/modules/core/store.js +236 -0
  15. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  16. package/server/agent/eval/modules/evaluation/runner.js +298 -0
  17. package/server/agent/eval/modules/evaluation/scoring.js +351 -0
  18. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  19. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  20. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  21. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  22. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  23. package/server/agent/eval/modules/ui/evalUi.js +4796 -0
  24. package/server/agent/eval/modules/utils/smokes.js +119 -0
  25. package/server/agent/ink/App.jsx +348 -0
  26. package/server/agent/ink/Panel.jsx +54 -0
  27. package/server/agent/ink/PromptInput.jsx +321 -0
  28. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  29. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  30. package/server/agent/ink/ToolResult.jsx +133 -0
  31. package/server/agent/ink/main.jsx +195 -0
  32. package/server/agent/ink/useEventBus.js +218 -0
  33. package/server/agent/memory/contextAllocator.js +214 -0
  34. package/server/agent/memory/hierarchicalStore.js +523 -0
  35. package/server/agent/memory/reflectionWorker.js +110 -0
  36. package/server/agent/prompts.js +1 -1
  37. package/server/agent/telemetry/hardwareTiers.js +332 -0
  38. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  39. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  40. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  41. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  42. package/server/agent/tests/agentFollowUps.test.js +127 -0
  43. package/server/agent/tests/apiRouter.test.js +108 -0
  44. package/server/agent/tests/architectureFixes.test.js +81 -0
  45. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  46. package/server/agent/tests/autoResearch.test.js +64 -0
  47. package/server/agent/tests/bilibiliEmbed.test.js +251 -0
  48. package/server/agent/tests/catalogSync.test.js +130 -0
  49. package/server/agent/tests/citations.test.js +180 -0
  50. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  51. package/server/agent/tests/clarifyingQuestions.test.js +594 -0
  52. package/server/agent/tests/cliAutomation.test.js +121 -0
  53. package/server/agent/tests/cloudProviders.test.js +234 -0
  54. package/server/agent/tests/commandInput.test.js +24 -0
  55. package/server/agent/tests/commands.test.js +417 -0
  56. package/server/agent/tests/contextIntelligence.test.js +162 -0
  57. package/server/agent/tests/conversationSafety.test.js +53 -0
  58. package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
  59. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  60. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  61. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  62. package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
  63. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  64. package/server/agent/tests/envBootstrap.test.js +87 -0
  65. package/server/agent/tests/envCommand.test.js +215 -0
  66. package/server/agent/tests/errorHandling.test.js +80 -0
  67. package/server/agent/tests/errors.test.js +13 -0
  68. package/server/agent/tests/evalAnalyze.test.js +54 -0
  69. package/server/agent/tests/evalCommand.test.js +54 -0
  70. package/server/agent/tests/evalCorpus.test.js +126 -0
  71. package/server/agent/tests/evalHarness.test.js +62 -0
  72. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  73. package/server/agent/tests/evalMetrics.test.js +312 -0
  74. package/server/agent/tests/evalStore.test.js +190 -0
  75. package/server/agent/tests/evalStudio.test.js +179 -0
  76. package/server/agent/tests/feedbackLearning.test.js +186 -0
  77. package/server/agent/tests/firstRun.test.js +44 -0
  78. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  79. package/server/agent/tests/hardwareTiers.test.js +206 -0
  80. package/server/agent/tests/hybridWebSearch.test.js +192 -0
  81. package/server/agent/tests/judgeService.test.js +251 -0
  82. package/server/agent/tests/lanDiscovery.test.js +319 -0
  83. package/server/agent/tests/mcpClient.test.js +201 -0
  84. package/server/agent/tests/modelManager.test.js +109 -0
  85. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  86. package/server/agent/tests/newCommands.test.js +347 -0
  87. package/server/agent/tests/newCommandsTui.test.js +141 -0
  88. package/server/agent/tests/newTools.test.js +113 -0
  89. package/server/agent/tests/newsIntelligence.test.js +178 -0
  90. package/server/agent/tests/nextGenArchitecture.test.js +99 -0
  91. package/server/agent/tests/offlineInput.test.js +217 -0
  92. package/server/agent/tests/packagingGuard.test.js +57 -0
  93. package/server/agent/tests/presentation.test.js +61 -0
  94. package/server/agent/tests/providerPersistence.test.js +39 -0
  95. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  96. package/server/agent/tests/queryDetails.test.js +126 -0
  97. package/server/agent/tests/realWorldValidation.test.js +391 -0
  98. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  99. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  100. package/server/agent/tests/sanitizer.test.js +37 -0
  101. package/server/agent/tests/scenariosData.js +378 -0
  102. package/server/agent/tests/schemaRouting.test.js +54 -0
  103. package/server/agent/tests/securityAuditRemediation.test.js +214 -0
  104. package/server/agent/tests/securityHardening.test.js +146 -0
  105. package/server/agent/tests/securitySanitizer.test.js +38 -0
  106. package/server/agent/tests/sessionAndCopy.test.js +232 -0
  107. package/server/agent/tests/sidebarControls.test.js +101 -0
  108. package/server/agent/tests/statefulMemory.test.js +270 -0
  109. package/server/agent/tests/subagent.test.js +216 -0
  110. package/server/agent/tests/subagentProgress.test.js +109 -0
  111. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  112. package/server/agent/tests/suggestions.test.js +199 -0
  113. package/server/agent/tests/suggestionsTui.test.js +251 -0
  114. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  115. package/server/agent/tests/terminalImage.test.js +80 -0
  116. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  117. package/server/agent/tests/terminalText.test.js +133 -0
  118. package/server/agent/tests/textProtocolLoop.test.js +126 -0
  119. package/server/agent/tests/textToolParsing.test.js +139 -0
  120. package/server/agent/tests/theme.test.js +26 -0
  121. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  122. package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
  123. package/server/agent/tests/tools.test.js +259 -0
  124. package/server/agent/tests/unifiedEndpoints.test.js +152 -0
  125. package/server/agent/tests/v110Features.test.js +262 -0
  126. package/server/agent/tests/versionBump.test.js +69 -0
  127. package/server/agent/tests/vramEstimator.test.js +520 -0
  128. package/server/agent/tests/webChatInspection.test.js +300 -0
  129. package/server/agent/tests/webChatMemory.test.js +112 -0
  130. package/server/agent/tests/webChatSecurity.test.js +169 -0
  131. package/server/agent/tests/webExtract.test.js +261 -0
  132. package/server/agent/tests/webSources.test.js +247 -0
  133. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  134. package/server/agent/websearch/adapters/jina.js +77 -0
  135. package/server/agent/websearch/adapters/searxng.js +57 -0
  136. package/server/agent/websearch/core/canonical.js +89 -0
  137. package/server/agent/websearch/core/fusion.js +81 -0
  138. package/server/agent/websearch/core/types.js +29 -0
  139. package/server/agent/websearch/extract.js +605 -0
  140. package/server/agent/websearch/index.js +43 -0
  141. package/server/agent/websearch/orchestrator.js +266 -0
  142. package/server/agent/websearch/resilience/breaker.js +97 -0
  143. package/server/agent/websearch/resilience/cache.js +59 -0
  144. package/server/agent/websearch/resilience/limiter.js +93 -0
  145. package/server/agent/websearch/security/filter.js +36 -0
  146. package/server/agent/websearch/security/ssrf.js +289 -0
  147. package/server/agent/websearch/telemetry.js +71 -0
  148. package/server/apiRouter.js +7 -11
  149. package/server/eval/judgeRubrics.js +110 -0
  150. package/server/eval/judgeService.js +290 -0
  151. package/server/eval/runBenchmark.js +182 -0
  152. package/server/mcp/examples/librechat.example.yaml +16 -0
  153. package/server/mcp/tests/connection.test.js +156 -0
  154. package/server/routes/agent.js +55 -0
  155. package/server/supabase_schema.sql +63 -0
@@ -0,0 +1,321 @@
1
+ import React, { useState, useCallback, useEffect, useRef } from 'react';
2
+ import { Box, Text, useInput, useApp } from 'ink';
3
+ import { toolsSchema } from '../tools.js';
4
+ import { INPUT_PLACEHOLDER, previousGraphemeBoundary, nextGraphemeBoundary } from '../ui.js';
5
+ import { COMMAND_DEFINITIONS, SUB_COMMANDS } from '../commandRegistry.js';
6
+ import { getInkPalette } from '../theme.js';
7
+
8
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
9
+
10
+ function getSuggestions(line) {
11
+ const trimmed = line.trimStart();
12
+ if (!trimmed.startsWith('/')) return [];
13
+
14
+ const parts = trimmed.split(/\s+/);
15
+ const baseCmd = parts[0].toLowerCase();
16
+
17
+ if (baseCmd === '/tool' && (parts.length > 1 || line.includes(' '))) {
18
+ const q = parts.slice(1).join(' ').toLowerCase();
19
+ if (!q) {
20
+ return toolsSchema.slice(0, 8).map(t => ({
21
+ name: `/tool ${t.function.name}`,
22
+ desc: t.function.description.slice(0, 50),
23
+ insert: `/tool ${t.function.name} `,
24
+ }));
25
+ }
26
+ return toolsSchema
27
+ .filter(t => t.function.name.toLowerCase().includes(q))
28
+ .slice(0, 8)
29
+ .map(t => ({
30
+ name: `/tool ${t.function.name}`,
31
+ desc: t.function.description.slice(0, 50),
32
+ insert: `/tool ${t.function.name} `,
33
+ }));
34
+ }
35
+
36
+ if (parts.length > 1 || (parts.length === 1 && line.includes(' '))) {
37
+ const subs = SUB_COMMANDS[baseCmd] || [];
38
+ const query = trimmed.toLowerCase();
39
+ return subs
40
+ .filter(s => s.toLowerCase().includes(query) || `${baseCmd} ${s}`.includes(query))
41
+ .map(s => ({
42
+ name: `${baseCmd} ${s}`,
43
+ desc: '',
44
+ insert: `${baseCmd} ${s} `,
45
+ }));
46
+ }
47
+
48
+ return COMMAND_DEFINITIONS
49
+ .map(c => ({
50
+ ...c,
51
+ score: c.name === baseCmd ? 200 : c.name.startsWith(baseCmd) ? 100 : c.name.includes(baseCmd) ? 70 : 0,
52
+ }))
53
+ .filter(c => c.score > 0)
54
+ .sort((a, b) => b.score - a.score);
55
+ }
56
+
57
+ function getGhostText(line, suggestions) {
58
+ if (!suggestions.length || !line.startsWith('/')) return '';
59
+ const top = suggestions[0];
60
+ const target = top.insert || top.name;
61
+ if (target.toLowerCase().startsWith(line.toLowerCase()) && target.length > line.length) {
62
+ return target.slice(line.length);
63
+ }
64
+ return '';
65
+ }
66
+
67
+ export function PromptInput({ onSubmit, musicStatus = '', isProcessing = false, agentStateLabel = '' }) {
68
+ const theme = getInkPalette();
69
+ const [value, setValue] = useState('');
70
+ const [cursorPos, setCursorPos] = useState(0);
71
+ const [suggestions, setSuggestions] = useState([]);
72
+ const [suggestionIdx, setSuggestionIdx] = useState(0);
73
+ const [showSuggestions, setShowSuggestions] = useState(false);
74
+ const [history, setHistory] = useState([]);
75
+ const [historyIdx, setHistoryIdx] = useState(-1);
76
+ const [spinFrame, setSpinFrame] = useState(0);
77
+ const { exit } = useApp();
78
+
79
+ useEffect(() => {
80
+ if (!isProcessing) return;
81
+ const id = setInterval(() => {
82
+ setSpinFrame(prev => (prev + 1) % SPINNER_FRAMES.length);
83
+ }, 80);
84
+ return () => clearInterval(id);
85
+ }, [isProcessing]);
86
+
87
+ useEffect(() => {
88
+ if (value.startsWith('/')) {
89
+ const s = getSuggestions(value);
90
+ setSuggestions(s);
91
+ setShowSuggestions(s.length > 0);
92
+ setSuggestionIdx(0);
93
+ } else {
94
+ setSuggestions([]);
95
+ setShowSuggestions(false);
96
+ }
97
+ }, [value]);
98
+
99
+ const ghostText = getGhostText(value, suggestions);
100
+ const maxVisibleSuggestions = 6;
101
+ const visibleSuggestionStart = Math.max(
102
+ 0,
103
+ Math.min(suggestionIdx - 2, suggestions.length - maxVisibleSuggestions),
104
+ );
105
+ const visibleSuggestions = suggestions.slice(
106
+ visibleSuggestionStart,
107
+ visibleSuggestionStart + maxVisibleSuggestions,
108
+ );
109
+
110
+ const statusText = isProcessing
111
+ ? `${SPINNER_FRAMES[spinFrame]} ${agentStateLabel || 'Processing…'}`
112
+ : musicStatus;
113
+
114
+ useInput((input, key) => {
115
+ if (key.ctrl && input === 'c') {
116
+ if (value.length > 0) {
117
+ setValue('');
118
+ setCursorPos(0);
119
+ return;
120
+ }
121
+ exit();
122
+ return;
123
+ }
124
+
125
+ if (key.ctrl && input === 'l') {
126
+ return;
127
+ }
128
+
129
+ if (key.escape) {
130
+ if (showSuggestions) {
131
+ setShowSuggestions(false);
132
+ return;
133
+ }
134
+ return;
135
+ }
136
+
137
+ if (key.tab) {
138
+ if (showSuggestions && suggestions.length > 0) {
139
+ const item = suggestions[suggestionIdx] || suggestions[0];
140
+ const insertVal = item.insert || item.name;
141
+ setValue(insertVal);
142
+ setCursorPos(insertVal.length);
143
+ if (!insertVal.endsWith(' ')) {
144
+ onSubmit(insertVal);
145
+ setHistory(prev => [...prev, insertVal]);
146
+ setValue('');
147
+ setCursorPos(0);
148
+ }
149
+ return;
150
+ }
151
+ if (ghostText) {
152
+ const newVal = value + ghostText;
153
+ setValue(newVal);
154
+ setCursorPos(newVal.length);
155
+ return;
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (key.return) {
161
+ if (showSuggestions && suggestions.length > 0) {
162
+ const item = suggestions[suggestionIdx] || suggestions[0];
163
+ const insertVal = item.insert || item.name;
164
+ if (insertVal.endsWith(' ')) {
165
+ setValue(insertVal);
166
+ setCursorPos(insertVal.length);
167
+ setShowSuggestions(false);
168
+ return;
169
+ }
170
+ onSubmit(insertVal);
171
+ setHistory(prev => [...prev, insertVal]);
172
+ setValue('');
173
+ setCursorPos(0);
174
+ setShowSuggestions(false);
175
+ return;
176
+ }
177
+ if (value.trim()) {
178
+ onSubmit(value);
179
+ setHistory(prev => [...prev, value]);
180
+ }
181
+ setValue('');
182
+ setCursorPos(0);
183
+ return;
184
+ }
185
+
186
+ if (key.up) {
187
+ if (showSuggestions && suggestions.length > 0) {
188
+ setSuggestionIdx(prev => (prev - 1 + suggestions.length) % suggestions.length);
189
+ return;
190
+ }
191
+ if (history.length > 0) {
192
+ const newIdx = historyIdx === -1 ? history.length - 1 : Math.max(0, historyIdx - 1);
193
+ setHistoryIdx(newIdx);
194
+ const cmd = history[newIdx] || '';
195
+ setValue(cmd);
196
+ setCursorPos(cmd.length);
197
+ }
198
+ return;
199
+ }
200
+
201
+ if (key.down) {
202
+ if (showSuggestions && suggestions.length > 0) {
203
+ setSuggestionIdx(prev => (prev + 1) % suggestions.length);
204
+ return;
205
+ }
206
+ if (historyIdx !== -1) {
207
+ const newIdx = historyIdx + 1;
208
+ if (newIdx >= history.length) {
209
+ setHistoryIdx(-1);
210
+ setValue('');
211
+ setCursorPos(0);
212
+ } else {
213
+ setHistoryIdx(newIdx);
214
+ const cmd = history[newIdx] || '';
215
+ setValue(cmd);
216
+ setCursorPos(cmd.length);
217
+ }
218
+ }
219
+ return;
220
+ }
221
+
222
+ if (key.backspace) {
223
+ if (cursorPos > 0) {
224
+ const previous = previousGraphemeBoundary(value, cursorPos);
225
+ setValue(value.slice(0, previous) + value.slice(cursorPos));
226
+ setCursorPos(previous);
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (key.delete) {
232
+ if (cursorPos < value.length) {
233
+ const next = nextGraphemeBoundary(value, cursorPos);
234
+ setValue(value.slice(0, cursorPos) + value.slice(next));
235
+ }
236
+ return;
237
+ }
238
+
239
+ if (key.home || (key.ctrl && input === 'a')) {
240
+ setCursorPos(0);
241
+ return;
242
+ }
243
+
244
+ if (key.end || (key.ctrl && input === 'e')) {
245
+ setCursorPos(value.length);
246
+ return;
247
+ }
248
+
249
+ if (key.left) {
250
+ setCursorPos(previousGraphemeBoundary(value, cursorPos));
251
+ return;
252
+ }
253
+
254
+ if (key.right) {
255
+ setCursorPos(nextGraphemeBoundary(value, cursorPos));
256
+ return;
257
+ }
258
+
259
+ if (input && !key.ctrl && !key.meta) {
260
+ if (input.startsWith('\x1b') || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(input)) return;
261
+ const newVal = value.slice(0, cursorPos) + input + value.slice(cursorPos);
262
+ setValue(newVal);
263
+ setCursorPos(prev => prev + input.length);
264
+ }
265
+ });
266
+
267
+ return (
268
+ <Box flexDirection="column">
269
+ <Box
270
+ backgroundColor={isProcessing ? theme.panelHeader : theme.panel}
271
+ width="100%"
272
+ paddingLeft={1}
273
+ paddingRight={1}
274
+ >
275
+ <Text color={isProcessing ? theme.info : theme.muted}>{statusText || ' '}</Text>
276
+ </Box>
277
+
278
+ {showSuggestions && suggestions.length > 0 && (
279
+ <Box
280
+ flexDirection="column"
281
+ width="100%"
282
+ backgroundColor={theme.panel}
283
+ paddingLeft={1}
284
+ paddingRight={1}
285
+ >
286
+ {visibleSuggestions.map((item, idx) => {
287
+ const actualIdx = visibleSuggestionStart + idx;
288
+ const isSelected = actualIdx === suggestionIdx;
289
+ return (
290
+ <Box key={item.name}>
291
+ <Text backgroundColor={theme.panel} color={isSelected ? theme.highlight : theme.text}>{isSelected ? '❯ ' : ' '}</Text>
292
+ <Text
293
+ bold={isSelected}
294
+ color={isSelected ? theme.text : theme.info}
295
+ backgroundColor={theme.panel}
296
+ >
297
+ {item.name.padEnd(22)}
298
+ </Text>
299
+ <Text backgroundColor={theme.panel} color={isSelected ? theme.text : theme.muted} dimColor={!isSelected}>
300
+ {item.desc}{' '}
301
+ </Text>
302
+ </Box>
303
+ );
304
+ })}
305
+ </Box>
306
+ )}
307
+
308
+ <Box backgroundColor={theme.panel} width="100%" paddingY={1}>
309
+ <Text bold color={theme.primary}>Kiku ❯ </Text>
310
+ <Text color={theme.text}>{value}</Text>
311
+ {ghostText && (
312
+ <Text color={theme.subdued} dimColor>{ghostText}</Text>
313
+ )}
314
+ {!value && (
315
+ <Text color={theme.muted} dimColor>{INPUT_PLACEHOLDER}</Text>
316
+ )}
317
+ <Text> </Text>
318
+ </Box>
319
+ </Box>
320
+ );
321
+ }
@@ -0,0 +1,88 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { getInkPalette } from '../theme.js';
4
+
5
+ const RUNNING_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+
7
+ export function TaskPlanPanel({ plan }) {
8
+ const [frameIdx, setFrameIdx] = useState(0);
9
+ const theme = getInkPalette();
10
+
11
+ useEffect(() => {
12
+ if (!plan?.isActive) return;
13
+ const id = setInterval(() => {
14
+ setFrameIdx(prev => (prev + 1) % RUNNING_FRAMES.length);
15
+ }, 70);
16
+ return () => clearInterval(id);
17
+ }, [plan?.isActive]);
18
+
19
+ if (!plan || !plan.steps || plan.steps.length === 0) return null;
20
+
21
+ const total = plan.steps.length;
22
+ const completed = plan.steps.filter(s => s.state === 'complete').length;
23
+ const pct = Math.round((completed / total) * 100);
24
+ const barFilled = Math.round((completed / total) * 10);
25
+ const barEmpty = 10 - barFilled;
26
+ const progressBar = `[${'█'.repeat(barFilled)}${'░'.repeat(barEmpty)}] ${pct}%`;
27
+
28
+ return (
29
+ <Box flexDirection="column" marginY={1} paddingX={1} borderStyle="single" borderColor={theme.subdued}>
30
+ <Box justifyContent="space-between">
31
+ <Text>
32
+ <Text bold color={theme.primary}>◆ Task Plan </Text>
33
+ <Text color={theme.highlight}>{progressBar} </Text>
34
+ <Text color={theme.muted}>({completed}/{total} steps)</Text>
35
+ </Text>
36
+ {plan.isActive && (
37
+ <Text color={theme.info}>
38
+ {RUNNING_FRAMES[frameIdx]} running
39
+ </Text>
40
+ )}
41
+ </Box>
42
+
43
+ {plan.question && (
44
+ <Box>
45
+ <Text color={theme.muted}>
46
+ │ Goal: "{plan.question.length > 56 ? plan.question.slice(0, 56) + '…' : plan.question}"
47
+ </Text>
48
+ </Box>
49
+ )}
50
+
51
+ {plan.steps.map((step, idx) => {
52
+ const isLast = idx === total - 1;
53
+ const branch = isLast ? '└─ ' : '├─ ';
54
+
55
+ let icon = '○';
56
+ let color = theme.muted;
57
+ let stateLabel = 'pending';
58
+
59
+ if (step.state === 'complete') {
60
+ icon = '✓';
61
+ color = theme.success;
62
+ stateLabel = 'done';
63
+ } else if (step.state === 'running') {
64
+ icon = '●';
65
+ color = theme.info;
66
+ stateLabel = 'active';
67
+ } else if (step.state === 'failed') {
68
+ icon = '✕';
69
+ color = theme.error;
70
+ stateLabel = 'failed';
71
+ }
72
+
73
+ return (
74
+ <Box key={step.id || idx}>
75
+ <Text color={theme.muted}>{branch}</Text>
76
+ <Text color={color} bold>{icon} </Text>
77
+ <Text color={theme.muted}>Step {idx + 1}: </Text>
78
+ <Text bold color={theme.text}>{step.label || step.id}</Text>
79
+ {step.objective && (
80
+ <Text color={theme.muted} dimColor> — {step.objective.length > 40 ? step.objective.slice(0, 40) + '…' : step.objective}</Text>
81
+ )}
82
+ <Text color={color}> [{stateLabel}]</Text>
83
+ </Box>
84
+ );
85
+ })}
86
+ </Box>
87
+ );
88
+ }
@@ -0,0 +1,68 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { getInkPalette } from '../theme.js';
4
+
5
+ const DOTS_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+
7
+ export function ThinkingPanel({ text, isActive, duration }) {
8
+ const [frameIdx, setFrameIdx] = useState(0);
9
+ const theme = getInkPalette();
10
+
11
+ useEffect(() => {
12
+ if (!isActive) return;
13
+ const id = setInterval(() => {
14
+ setFrameIdx(prev => (prev + 1) % DOTS_FRAMES.length);
15
+ }, 80);
16
+ return () => clearInterval(id);
17
+ }, [isActive]);
18
+
19
+ if (!isActive && (!text || text.length === 0)) return null;
20
+
21
+ const icon = isActive
22
+ ? DOTS_FRAMES[frameIdx]
23
+ : '[DONE]';
24
+
25
+ const statusColor = isActive ? theme.tertiary : theme.success;
26
+
27
+ const cleanText = (text || '')
28
+ .replace(/\*\*\*?([^*]+)\*\*\*/g, '$1')
29
+ .replace(/\*([^\*\n]+)\*/g, '$1')
30
+ .replace(/^(\s*)[-*]\s+/gm, '$1• ')
31
+ .replace(/\*/g, '')
32
+ .trim();
33
+
34
+ const lines = cleanText.split('\n').filter(l => l.trim());
35
+
36
+ return (
37
+ <Box flexDirection="column">
38
+ <Box>
39
+ <Text> </Text>
40
+ <Text color={statusColor} bold>{icon} </Text>
41
+ <Text color={statusColor} bold>
42
+ {isActive ? 'Thinking…' : `Thought for ${duration}s`}
43
+ </Text>
44
+ </Box>
45
+
46
+ {isActive && lines.length > 0 && (
47
+ <Box flexDirection="column" paddingLeft={1}>
48
+ {lines.slice(0, 15).map((line, i) => (
49
+ <Box key={i}>
50
+ <Text color={theme.muted} dimColor> {line}</Text>
51
+ </Box>
52
+ ))}
53
+ {lines.length > 15 && (
54
+ <Box>
55
+ <Text color={theme.muted} dimColor> ... +{lines.length - 15} more lines</Text>
56
+ </Box>
57
+ )}
58
+ </Box>
59
+ )}
60
+
61
+ {!isActive && duration && (
62
+ <Box>
63
+ <Text> </Text>
64
+ </Box>
65
+ )}
66
+ </Box>
67
+ );
68
+ }
@@ -0,0 +1,133 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { getInkPalette } from '../theme.js';
4
+
5
+ const RUNNING_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+
7
+ export function ToolRunning({ name, args }) {
8
+ const [frameIdx, setFrameIdx] = useState(0);
9
+ const theme = getInkPalette();
10
+
11
+ useEffect(() => {
12
+ const id = setInterval(() => {
13
+ setFrameIdx(prev => (prev + 1) % RUNNING_FRAMES.length);
14
+ }, 70);
15
+ return () => clearInterval(id);
16
+ }, []);
17
+
18
+ const queryVal = args?.query || args?.url || args?.endpoint || '';
19
+ const displayQuery = typeof queryVal === 'string' && queryVal.length > 35
20
+ ? queryVal.slice(0, 35) + '…'
21
+ : queryVal;
22
+
23
+ return (
24
+ <Box>
25
+ <Text> </Text>
26
+ <Text color={theme.info}>{RUNNING_FRAMES[frameIdx]} </Text>
27
+ <Text bold color={theme.text}>{name}</Text>
28
+ {displayQuery && (
29
+ <Text color={theme.muted}> "{displayQuery}"</Text>
30
+ )}
31
+ </Box>
32
+ );
33
+ }
34
+
35
+ export function ToolCompleted({ name, result, duration }) {
36
+ const theme = getInkPalette();
37
+ const isError = result?.status === 'error';
38
+ const isNotFound = result?.status === 'not_found';
39
+
40
+ const statusLabel = isError ? '[ERROR]' : isNotFound ? '[NOT FOUND]' : '[OK]';
41
+ const iconColor = isError
42
+ ? theme.error
43
+ : isNotFound
44
+ ? theme.warning
45
+ : theme.success;
46
+
47
+ const statusText = isError ? 'Failed' : isNotFound ? 'Not found' : 'OK';
48
+ const count = result?.count ?? result?.returnedCount;
49
+ const countTag = count !== null && !isError ? ` • ${count} results` : '';
50
+ const durationTag = duration ? ` • ${(duration / 1000).toFixed(2)}s` : '';
51
+
52
+ const preview = extractPreview(result);
53
+
54
+ return (
55
+ <Box flexDirection="column">
56
+ <Box>
57
+ <Text> </Text>
58
+ <Text color={theme.muted}>▸ </Text>
59
+ <Text bold color={theme.info}>{name}</Text>
60
+ <Text> </Text>
61
+ <Text color={iconColor} bold>{statusLabel}</Text>
62
+ <Text color={iconColor}> {statusText}</Text>
63
+ {countTag && <Text color={theme.info}>{countTag}</Text>}
64
+ {durationTag && <Text color={theme.muted} dimColor>{durationTag}</Text>}
65
+ </Box>
66
+ {preview && (
67
+ <Box paddingLeft={1}>
68
+ <Text color={theme.muted} dimColor>• {preview}</Text>
69
+ </Box>
70
+ )}
71
+ </Box>
72
+ );
73
+ }
74
+
75
+ export function ToolSummary({ tools }) {
76
+ if (!tools || tools.length === 0) return null;
77
+ const theme = getInkPalette();
78
+
79
+ return (
80
+ <Box flexDirection="column">
81
+ <Box>
82
+ <Text> </Text>
83
+ <Text color={theme.info} bold>▾ Tool Invocations ({tools.length})</Text>
84
+ </Box>
85
+ {tools.map((t, idx) => {
86
+ const statusLabel = t.status === 'error' ? '[ERROR]' : t.status === 'running' ? '[RUNNING]' : '[OK]';
87
+ const iconColor = t.status === 'error'
88
+ ? theme.error
89
+ : t.status === 'running'
90
+ ? theme.warning
91
+ : theme.success;
92
+ return (
93
+ <Box key={idx}>
94
+ <Text> </Text>
95
+ <Text color={theme.highlight}>[{idx + 1}]</Text>
96
+ <Text> </Text>
97
+ <Text color={theme.info} bold>{t.name}</Text>
98
+ <Text> </Text>
99
+ <Text color={iconColor}>{statusLabel}</Text>
100
+ {t.duration > 0 && (
101
+ <Text color={theme.muted} dimColor> {(t.duration / 1000).toFixed(2)}s</Text>
102
+ )}
103
+ </Box>
104
+ );
105
+ })}
106
+ </Box>
107
+ );
108
+ }
109
+
110
+ function extractPreview(result) {
111
+ if (!result) return '';
112
+ const MAX = 80;
113
+ let raw = '';
114
+
115
+ if (result.content) raw = result.content;
116
+ else if (result.verdict) raw = `[Verdict: ${result.verdict}] ${result.summary || ''}`;
117
+ else if (result.subagentReport) raw = `[Subagent] ${result.subagentReport}`;
118
+ else if (result.researchReport) raw = `[Research: ${result.sourcesCount || 0} sources] ${result.researchReport}`;
119
+ else if (result.answer) raw = String(result.answer);
120
+ else if (result.results && Array.isArray(result.results) && result.results.length > 0) {
121
+ raw = result.results[0].title || result.results[0].snippet || '';
122
+ } else if (result.works) raw = result.works.map(w => w.titleTh || w.titleCn).filter(Boolean).join(', ');
123
+ else if (result.songs) raw = result.songs.map(s => s.title).filter(Boolean).join(', ');
124
+ else if (result.brands) raw = result.brands.map(b => b.brand).filter(Boolean).join(', ');
125
+ else if (result.profile) raw = `${result.profile.nameTh || ''} ${result.profile.nameCn || ''}`.trim();
126
+ else if (result.stats) raw = `${result.stats.totalWorks || 0} works, ${result.stats.totalUniqueSongs || 0} songs`;
127
+ else if (result.skills) raw = `Skills (${result.skills.length}) enabled: ${result.skillsEnabled}`;
128
+ else if (result.message) raw = result.message;
129
+
130
+ if (!raw) return '';
131
+ raw = raw.replace(/\s+/g, ' ').trim();
132
+ return raw.length > MAX ? raw.slice(0, MAX) + '…' : raw;
133
+ }