thatgfsj-code 3.0.4 → 3.0.7

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 (205) hide show
  1. package/README.md +64 -21
  2. package/dist/app/index.d.ts +93 -22
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +232 -40
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cmd/index.d.ts +12 -20
  7. package/dist/cmd/index.d.ts.map +1 -1
  8. package/dist/cmd/index.js +192 -146
  9. package/dist/cmd/index.js.map +1 -1
  10. package/dist/config/index.d.ts +4 -0
  11. package/dist/config/index.d.ts.map +1 -1
  12. package/dist/config/index.js +5 -0
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/config/types.d.ts +5 -0
  15. package/dist/config/types.d.ts.map +1 -1
  16. package/dist/llm/anthropic.d.ts +1 -1
  17. package/dist/llm/anthropic.d.ts.map +1 -1
  18. package/dist/llm/anthropic.js +116 -84
  19. package/dist/llm/anthropic.js.map +1 -1
  20. package/dist/llm/gemini.d.ts +27 -3
  21. package/dist/llm/gemini.d.ts.map +1 -1
  22. package/dist/llm/gemini.js +196 -73
  23. package/dist/llm/gemini.js.map +1 -1
  24. package/dist/llm/index.d.ts +20 -1
  25. package/dist/llm/index.d.ts.map +1 -1
  26. package/dist/llm/index.js +66 -6
  27. package/dist/llm/index.js.map +1 -1
  28. package/dist/llm/openai.d.ts +5 -1
  29. package/dist/llm/openai.d.ts.map +1 -1
  30. package/dist/llm/openai.js +126 -66
  31. package/dist/llm/openai.js.map +1 -1
  32. package/dist/mcp/client.d.ts +84 -34
  33. package/dist/mcp/client.d.ts.map +1 -1
  34. package/dist/mcp/client.js +291 -126
  35. package/dist/mcp/client.js.map +1 -1
  36. package/dist/session/compactor.d.ts +27 -7
  37. package/dist/session/compactor.d.ts.map +1 -1
  38. package/dist/session/compactor.js +78 -23
  39. package/dist/session/compactor.js.map +1 -1
  40. package/dist/session/index.d.ts +96 -33
  41. package/dist/session/index.d.ts.map +1 -1
  42. package/dist/session/index.js +278 -60
  43. package/dist/session/index.js.map +1 -1
  44. package/dist/tools/file.d.ts +10 -2
  45. package/dist/tools/file.d.ts.map +1 -1
  46. package/dist/tools/file.js +54 -5
  47. package/dist/tools/file.js.map +1 -1
  48. package/dist/tools/git.d.ts +16 -2
  49. package/dist/tools/git.d.ts.map +1 -1
  50. package/dist/tools/git.js +61 -18
  51. package/dist/tools/git.js.map +1 -1
  52. package/dist/tools/nwt.d.ts.map +1 -1
  53. package/dist/tools/nwt.js +8 -1
  54. package/dist/tools/nwt.js.map +1 -1
  55. package/dist/tools/search.d.ts +8 -1
  56. package/dist/tools/search.d.ts.map +1 -1
  57. package/dist/tools/search.js +105 -34
  58. package/dist/tools/search.js.map +1 -1
  59. package/dist/tools/shell.d.ts +0 -4
  60. package/dist/tools/shell.d.ts.map +1 -1
  61. package/dist/tools/shell.js +11 -26
  62. package/dist/tools/shell.js.map +1 -1
  63. package/dist/tools/types.d.ts +7 -0
  64. package/dist/tools/types.d.ts.map +1 -1
  65. package/dist/tools/types.js.map +1 -1
  66. package/dist/tui/app.d.ts.map +1 -1
  67. package/dist/tui/app.js +90 -18
  68. package/dist/tui/app.js.map +1 -1
  69. package/dist/tui/components/ChatMessage.d.ts.map +1 -1
  70. package/dist/tui/components/ChatMessage.js +9 -2
  71. package/dist/tui/components/ChatMessage.js.map +1 -1
  72. package/dist/tui/components/ConfirmPrompt.d.ts +21 -0
  73. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  74. package/dist/tui/components/ConfirmPrompt.js +45 -0
  75. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  76. package/dist/tui/components/Header.d.ts +10 -5
  77. package/dist/tui/components/Header.d.ts.map +1 -1
  78. package/dist/tui/components/Header.js +13 -5
  79. package/dist/tui/components/Header.js.map +1 -1
  80. package/dist/tui/components/StatusBar.d.ts +7 -0
  81. package/dist/tui/components/StatusBar.d.ts.map +1 -1
  82. package/dist/tui/components/StatusBar.js +8 -3
  83. package/dist/tui/components/StatusBar.js.map +1 -1
  84. package/dist/tui/components/Thinking.d.ts +5 -0
  85. package/dist/tui/components/Thinking.d.ts.map +1 -1
  86. package/dist/tui/components/Thinking.js +24 -2
  87. package/dist/tui/components/Thinking.js.map +1 -1
  88. package/dist/tui/components/ToolCall.d.ts +6 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +50 -22
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts +5 -0
  93. package/dist/tui/components/UserInput.d.ts.map +1 -1
  94. package/dist/tui/components/UserInput.js +9 -13
  95. package/dist/tui/components/UserInput.js.map +1 -1
  96. package/dist/tui/hooks/useChat.d.ts +1 -0
  97. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  98. package/dist/tui/hooks/useChat.js +36 -16
  99. package/dist/tui/hooks/useChat.js.map +1 -1
  100. package/dist/tui/hooks/useCommands.d.ts +7 -1
  101. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  102. package/dist/tui/hooks/useCommands.js +77 -41
  103. package/dist/tui/hooks/useCommands.js.map +1 -1
  104. package/dist/tui/theme.d.ts +34 -0
  105. package/dist/tui/theme.d.ts.map +1 -0
  106. package/dist/tui/theme.js +34 -0
  107. package/dist/tui/theme.js.map +1 -0
  108. package/dist/tui/welcome.d.ts.map +1 -1
  109. package/dist/tui/welcome.js +9 -7
  110. package/dist/tui/welcome.js.map +1 -1
  111. package/dist/types.d.ts +16 -1
  112. package/dist/types.d.ts.map +1 -1
  113. package/dist/utils/diff.d.ts +5 -0
  114. package/dist/utils/diff.d.ts.map +1 -1
  115. package/dist/utils/diff.js +15 -0
  116. package/dist/utils/diff.js.map +1 -1
  117. package/dist/version.d.ts +12 -0
  118. package/dist/version.d.ts.map +1 -0
  119. package/dist/version.js +29 -0
  120. package/dist/version.js.map +1 -0
  121. package/package.json +19 -7
  122. package/CHANGELOG.md +0 -192
  123. package/DEVELOPMENT.md +0 -286
  124. package/ROADMAP.md +0 -91
  125. package/docs/API_KEY_GUIDE.md +0 -236
  126. package/docs/FAQ.md +0 -182
  127. package/install.bat +0 -63
  128. package/install.ps1 +0 -238
  129. package/install.sh +0 -113
  130. package/src/app/index.ts +0 -197
  131. package/src/cache/fingerprint.ts +0 -101
  132. package/src/cache/index.ts +0 -21
  133. package/src/cache/smartModel.ts +0 -133
  134. package/src/cache/stats.ts +0 -199
  135. package/src/cache/volatile.ts +0 -47
  136. package/src/cmd/index.tsx +0 -288
  137. package/src/config/index.ts +0 -156
  138. package/src/config/providers.ts +0 -234
  139. package/src/config/types.ts +0 -71
  140. package/src/hooks/index.ts +0 -111
  141. package/src/llm/anthropic.ts +0 -413
  142. package/src/llm/gemini.ts +0 -169
  143. package/src/llm/index.ts +0 -303
  144. package/src/llm/openai.ts +0 -243
  145. package/src/llm/provider.ts +0 -71
  146. package/src/mcp/client.ts +0 -330
  147. package/src/prompts/index.ts +0 -260
  148. package/src/session/compactor.ts +0 -103
  149. package/src/session/index.ts +0 -181
  150. package/src/session/message.ts +0 -42
  151. package/src/skills/brainstorming.ts +0 -43
  152. package/src/skills/code-review.ts +0 -45
  153. package/src/skills/executing-plans.ts +0 -27
  154. package/src/skills/frontend-design.ts +0 -35
  155. package/src/skills/git-workflow.ts +0 -36
  156. package/src/skills/improve-architecture.ts +0 -38
  157. package/src/skills/index.ts +0 -136
  158. package/src/skills/neuroweave.ts +0 -47
  159. package/src/skills/playwright.ts +0 -72
  160. package/src/skills/prototype.ts +0 -30
  161. package/src/skills/subagent.ts +0 -28
  162. package/src/skills/supabase.ts +0 -44
  163. package/src/skills/systematic-debugging.ts +0 -44
  164. package/src/skills/tdd.ts +0 -39
  165. package/src/skills/triage.ts +0 -35
  166. package/src/skills/verification.ts +0 -31
  167. package/src/skills/writing-plans.ts +0 -42
  168. package/src/tools/file.ts +0 -143
  169. package/src/tools/git.ts +0 -132
  170. package/src/tools/index.ts +0 -111
  171. package/src/tools/nwt.ts +0 -598
  172. package/src/tools/search.ts +0 -263
  173. package/src/tools/shell.ts +0 -136
  174. package/src/tools/types.ts +0 -122
  175. package/src/tui/app.tsx +0 -159
  176. package/src/tui/components/ChatList.tsx +0 -41
  177. package/src/tui/components/ChatMessage.tsx +0 -54
  178. package/src/tui/components/Header.tsx +0 -66
  179. package/src/tui/components/InitWizard.tsx +0 -247
  180. package/src/tui/components/Markdown.tsx +0 -35
  181. package/src/tui/components/ModelSelector.tsx +0 -107
  182. package/src/tui/components/StatusBar.tsx +0 -30
  183. package/src/tui/components/Thinking.tsx +0 -17
  184. package/src/tui/components/ToolCall.tsx +0 -102
  185. package/src/tui/components/UserInput.tsx +0 -151
  186. package/src/tui/hooks/useChat.ts +0 -287
  187. package/src/tui/hooks/useCommands.ts +0 -271
  188. package/src/tui/index.ts +0 -6
  189. package/src/tui/welcome.ts +0 -177
  190. package/src/types.ts +0 -104
  191. package/src/utils/diff.ts +0 -71
  192. package/src/utils/project.ts +0 -99
  193. package/src/utils/stableStringify.ts +0 -47
  194. package/src/utils/thinking.ts +0 -119
  195. package/tests/cache/fingerprint.test.ts +0 -75
  196. package/tests/cache/providerCatalog.test.ts +0 -31
  197. package/tests/cache/smartModel.test.ts +0 -100
  198. package/tests/cache/stableStringify.test.ts +0 -43
  199. package/tests/cache/stats.test.ts +0 -146
  200. package/tests/cache/volatile.test.ts +0 -75
  201. package/tests/smoke-pollution.mjs +0 -78
  202. package/tests/smoke-thinking.mjs +0 -110
  203. package/tests/smoke-tool-stream.mjs +0 -69
  204. package/tests/smoke-tool.mjs +0 -117
  205. package/tsconfig.json +0 -21
@@ -1,75 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { VolatileScratch } from '../../src/cache/volatile.js';
3
-
4
- describe('VolatileScratch', () => {
5
- it('starts empty', () => {
6
- const s = new VolatileScratch();
7
- expect(s.isEmpty()).toBe(true);
8
- expect(s.read()).toBe('');
9
- expect(s.size()).toBe(0);
10
- });
11
-
12
- it('accumulates writes separated by newlines', () => {
13
- const s = new VolatileScratch();
14
- s.write('first thought');
15
- s.write('second thought');
16
- expect(s.read()).toBe('first thought\nsecond thought');
17
- expect(s.size()).toBe(2);
18
- expect(s.isEmpty()).toBe(false);
19
- });
20
-
21
- it('ignores empty writes', () => {
22
- const s = new VolatileScratch();
23
- s.write('');
24
- s.write('real content');
25
- s.write('');
26
- expect(s.size()).toBe(1);
27
- expect(s.read()).toBe('real content');
28
- });
29
-
30
- it('reset() clears everything', () => {
31
- const s = new VolatileScratch();
32
- s.write('a');
33
- s.write('b');
34
- s.reset();
35
- expect(s.isEmpty()).toBe(true);
36
- expect(s.read()).toBe('');
37
- expect(s.size()).toBe(0);
38
- });
39
-
40
- it('multiple reset() cycles are independent', () => {
41
- const s = new VolatileScratch();
42
- s.write('cycle 1');
43
- s.reset();
44
- s.write('cycle 2');
45
- expect(s.read()).toBe('cycle 2');
46
- s.reset();
47
- expect(s.read()).toBe('');
48
- });
49
- });
50
-
51
- describe('shouldDowngrade (smartModel)', () => {
52
- it('returns false when prompt is long', async () => {
53
- const { shouldDowngrade } = await import('../../src/cache/smartModel.js');
54
- const decision = shouldDowngrade([], 'x'.repeat(500));
55
- expect(decision.downgrade).toBe(false);
56
- expect(decision.reason).toBe('prompt-too-long');
57
- });
58
-
59
- it('returns false when recent messages had tool_calls', async () => {
60
- const { shouldDowngrade } = await import('../../src/cache/smartModel.js');
61
- const messages = [
62
- { role: 'assistant' as const, content: '...', tool_calls: [{ id: '1', type: 'function' as const, function: { name: 'file', arguments: '{}' } }] },
63
- ];
64
- const decision = shouldDowngrade(messages, 'short prompt');
65
- expect(decision.downgrade).toBe(false);
66
- expect(decision.reason).toBe('recent-tool-call');
67
- });
68
-
69
- it('returns true for short prompt with no recent tool activity', async () => {
70
- const { shouldDowngrade } = await import('../../src/cache/smartModel.js');
71
- const decision = shouldDowngrade([], 'thanks!');
72
- expect(decision.downgrade).toBe(true);
73
- expect(decision.reason).toBe('short-no-tools');
74
- });
75
- });
@@ -1,78 +0,0 @@
1
- // Smoke test: SessionManager.looksPolluted filter
2
- // Verifies the v2.2.4 anti-pollution filter catches the markers that
3
- // caused the [已中断] hallucination loop in v2.2.3.
4
-
5
- import { SessionManager } from '../dist/session/index.js';
6
-
7
- let pass = 0, fail = 0;
8
- const check = (name, cond) => {
9
- console.log((cond ? '✅' : '❌') + ' ' + name);
10
- cond ? pass++ : fail++;
11
- };
12
-
13
- const shouldDrop = (content) => {
14
- const sm = new SessionManager();
15
- return sm.addMessageSafe('assistant', content);
16
- };
17
-
18
- const shouldKeep = (content) => {
19
- const sm = new SessionManager();
20
- return sm.addMessageSafe('assistant', content);
21
- };
22
-
23
- // These SHOULD be dropped (matches the loop-causing pollution patterns)
24
- check('drop: [已中断] at line start',
25
- shouldDrop('[已中断]\npartial text') === false);
26
- check('drop: [已中断] standalone',
27
- shouldDrop('[已中断]') === false);
28
- check('drop: response was truncated',
29
- shouldDrop('Some text. response was truncated mid-stream.') === false);
30
- check('drop: output was cut off',
31
- shouldDrop('output was cut off, please retry') === false);
32
- check('drop: [interrupted] English',
33
- shouldDrop('[interrupted] partial assistant response') === false);
34
-
35
- // These SHOULD be kept (legitimate conversation mentioning these phrases)
36
- check('keep: 已中断 mentioned in middle of sentence',
37
- shouldKeep('我看到这个进程已中断了') === true);
38
- check('keep: "response truncated" as user complaint',
39
- shouldKeep('Why was the response truncated last time?') === true);
40
- check('keep: normal Chinese sentence',
41
- shouldKeep('好的,我已经理解了。') === true);
42
- check('keep: normal English',
43
- shouldKeep('Hello, how can I help you?') === true);
44
- check('keep: empty',
45
- shouldKeep('') === true); // empty doesn't match any pattern
46
-
47
- // Test getDroppedCount
48
- {
49
- const sm = new SessionManager();
50
- sm.addMessageSafe('assistant', '[已中断] partial');
51
- sm.addMessageSafe('assistant', 'normal response');
52
- sm.addMessageSafe('assistant', '[已中断] another partial');
53
- check('droppedCount === 2', sm.getDroppedCount() === 2);
54
- }
55
-
56
- // Test that kept messages actually go into getMessages()
57
- {
58
- const sm = new SessionManager();
59
- sm.addMessage('user', 'hello');
60
- sm.addMessageSafe('assistant', 'normal response');
61
- sm.addMessageSafe('assistant', '[已中断] dropped');
62
- const msgs = sm.getMessages();
63
- check('kept 2 messages after drop', msgs.length === 2);
64
- check('user msg preserved', msgs[0].content === 'hello');
65
- check('normal assistant preserved', msgs[1].content === 'normal response');
66
- }
67
-
68
- // Test length-gated WEAK pattern detection
69
- check('drop: short "response was truncated" (<200 chars)',
70
- shouldDrop('Some text. response was truncated mid-stream.') === false);
71
- check('drop: short "response was cut off" (<200 chars)',
72
- shouldDrop('response was cut off, please retry') === false);
73
- // Test length gate — long messages with same phrase are kept
74
- check('keep: long "Why was response truncated last time?" (>=200 chars)',
75
- shouldKeep('Why was the response truncated last time? It seemed to stop mid-sentence when the network went down. I think we should add retry logic so this kind of mid-stream failure does not cause confusion for the end user, who might wonder whether their question was actually answered correctly.' + 'x'.repeat(100)) === true);
76
-
77
- console.log(`\n=== ${pass} pass / ${fail} fail ===`);
78
- if (fail > 0) process.exit(1);
@@ -1,110 +0,0 @@
1
- // Smoke test: thinking compression on realistic samples
2
- import { splitThinking, summarizeThinking, compressThinking } from '../dist/utils/thinking.js';
3
-
4
- let pass = 0, fail = 0;
5
- const eq = (name, actual, expected) => {
6
- const ok = JSON.stringify(actual) === JSON.stringify(expected);
7
- console.log((ok ? '✅' : '❌') + ' ' + name);
8
- if (!ok) console.log(' actual :', JSON.stringify(actual), '\n expected:', JSON.stringify(expected));
9
- ok ? pass++ : fail++;
10
- };
11
- const truthy = (name, value) => {
12
- console.log((value ? '✅' : '❌') + ' ' + name);
13
- value ? pass++ : fail++;
14
- };
15
-
16
- // Case A: simple <think>...</think> with English hint
17
- {
18
- const s = splitThinking('<think>The user said hi. I should respond warmly.</think>\n\nHi there!');
19
- eq('A.thinkingLines', s.thinkingLines, 1);
20
- eq('A.thinkingHint', s.thinkingHint, 'The user said hi. I should respond warmly.');
21
- eq('A.conclusion', s.conclusion, 'Hi there!');
22
- }
23
-
24
- // Case B: multi-line <think> + Chinese conclusion (user-paste style)
25
- {
26
- const s = splitThinking(
27
- '<think>The user is asking in Chinese if I am minimax\\u0027s M3.\nBased on the ROOT_SYSTEM_POLICY, yes I am MiniMax-M3. I\nwill respond in Chinese since they asked in Chinese.</think>\n\n是的,我是 MiniMax-M3。'
28
- );
29
- truthy('B.thinkingLines >= 2', s.thinkingLines >= 2);
30
- eq('B.conclusion starts with 是', s.conclusion.startsWith('是的'), true);
31
- }
32
-
33
- // Case C: <reasoning>...</reasoning> alternative format
34
- {
35
- const s = splitThinking('<reasoning>step 1\nstep 2</reasoning>\nFinal answer here.');
36
- eq('C.thinkingLines', s.thinkingLines, 2);
37
- eq('C.thinkingHint', s.thinkingHint, 'step 1');
38
- eq('C.conclusion', s.conclusion, 'Final answer here.');
39
- }
40
-
41
- // Case D: [THINK]...[/THINK] alternative format
42
- {
43
- const s = splitThinking('[THINK]user greeted me[/THINK]\nGreetings!');
44
- eq('D.thinkingHint', s.thinkingHint, 'user greeted me');
45
- eq('D.conclusion', s.conclusion, 'Greetings!');
46
- }
47
-
48
- // Case E: no thinking block at all
49
- {
50
- const s = splitThinking('Just a plain response.');
51
- eq('E.thinkingLines', s.thinkingLines, 0);
52
- eq('E.thinkingHint', s.thinkingHint, '');
53
- eq('E.conclusion', s.conclusion, 'Just a plain response.');
54
- }
55
-
56
- // Case F: multiple <think> blocks (rare but possible)
57
- {
58
- const s = splitThinking('<think>first thought</think>middle text<think>second thought</think>\n\nFinal.');
59
- truthy('F.thinkingLines > 0', s.thinkingLines > 0);
60
- truthy('F.conclusion contains "Final."', s.conclusion.includes('Final.'));
61
- }
62
-
63
- // Case G: compressThinking round-trip
64
- {
65
- const original = '<think>lots of internal monologue spanning\nseveral lines about nothing in particular</think>\n\nShort answer.';
66
- const compressed = compressThinking(original, false);
67
- truthy('G.compressed has 💭', compressed.includes('💭'));
68
- truthy('G.compressed has "Short answer."', compressed.includes('Short answer.'));
69
- truthy('G.compressed strips <think>', !compressed.includes('<think>'));
70
- }
71
-
72
- // Case H: compressThinking with showThinking=true passthrough
73
- {
74
- const original = '<think>inner</think>\n\nouter';
75
- const out = compressThinking(original, true);
76
- truthy('H.showThinking=true preserves <think>', out.includes('<think>'));
77
- truthy('H.showThinking=true preserves outer', out.includes('outer'));
78
- }
79
-
80
- // Case I: empty input
81
- {
82
- const s = splitThinking('');
83
- eq('I.empty.thinkingLines', s.thinkingLines, 0);
84
- eq('I.empty.conclusion', s.conclusion, '');
85
- }
86
-
87
- // Case J: hint with line wrap across tag (regression test for tag-strip fix)
88
- {
89
- // raw content captured by regex includes '<think>' on its own line
90
- const raw = '<think>\nThe actual thought starts here.\nMore lines.\n</think>\n\nConclusion.';
91
- const s = splitThinking(raw);
92
- truthy('J.hint does not contain <think>', !s.thinkingHint.includes('<think>'));
93
- truthy('J.hint does not contain </think>', !s.thinkingHint.includes('</think>'));
94
- truthy('J.hint contains real content', s.thinkingHint.includes('actual thought'));
95
- }
96
-
97
- // Case K: realistic long thinking block (mimics user-paste scenario)
98
- {
99
- const longThink = '<think>'.repeat(1) +
100
- Array.from({length: 14}, (_, i) => `Line ${i+1} of internal monologue.`).join('\n') +
101
- '</think>\n\n' +
102
- '没死没死,只是被你那句"随便思考一下问题"给整不会了 😂';
103
- const out = compressThinking(longThink, false);
104
- truthy('K.compressed shows 14 lines', out.includes('thought for 14 lines'));
105
- truthy('K.compressed shows conclusion', out.includes('没死没死'));
106
- truthy('K.compressed is short (no 14 lines of monologue)', out.split('\n').length < 8);
107
- }
108
-
109
- console.log(`\n=== ${pass} pass / ${fail} fail ===`);
110
- if (fail > 0) process.exit(1);
@@ -1,69 +0,0 @@
1
- // Simulates the exact useChat stream-parsing logic from src/tui/hooks/useChat.ts
2
- // to confirm tool result is correctly extracted from streaming chunks.
3
-
4
- function simulateUseChat(chunks) {
5
- let fullContent = '';
6
- let currentToolCalls = [];
7
- const states = [];
8
-
9
- for (const chunk of chunks) {
10
- if (chunk.includes('@@TOOL@@')) {
11
- const parts = chunk.split('\n');
12
- for (const part of parts) {
13
- if (part.startsWith('@@TOOL@@')) {
14
- try {
15
- const data = JSON.parse(part.slice(8));
16
- if (data.action === 'call') {
17
- currentToolCalls.push({ name: data.name, args: data.args || '' });
18
- states.push({ kind: 'call', toolCalls: JSON.parse(JSON.stringify(currentToolCalls)) });
19
- } else if (data.action === 'result') {
20
- const lastIdx = currentToolCalls.length - 1;
21
- if (lastIdx >= 0) {
22
- currentToolCalls[lastIdx] = {
23
- ...currentToolCalls[lastIdx],
24
- result: data.output || data.error || '',
25
- isError: !!data.error,
26
- };
27
- }
28
- states.push({ kind: 'result', toolCalls: JSON.parse(JSON.stringify(currentToolCalls)) });
29
- }
30
- } catch (e) {
31
- fullContent += part;
32
- }
33
- } else if (part) {
34
- fullContent += part;
35
- }
36
- }
37
- } else {
38
- fullContent += chunk;
39
- }
40
- }
41
-
42
- return { fullContent, states, finalToolCalls: currentToolCalls };
43
- }
44
-
45
- // Simulate what LLMService.chatStream yields in a real session:
46
- // iter 1: tool_call → tool.execute → tool_result → \n → loop
47
- // iter 2: text "好嘞,那我给你演示..."
48
- const chunks = [
49
- '\n@@TOOL@@{"action":"call","name":"shell","args":"{\\"command\\":\\"date\\"}"}\n',
50
- '@@TOOL@@{"action":"result","output":"Mon Jul 6 22:00:00 CST 2026"}\n',
51
- '\n',
52
- '\n',
53
- '好',
54
- '嘞',
55
- ',',
56
- '那我',
57
- '给你',
58
- '演示一组工具——并行调用,互不干扰:',
59
- '\n',
60
- ];
61
-
62
- const result = simulateUseChat(chunks);
63
- console.log('--- states (push order) ---');
64
- result.states.forEach((s, i) => console.log(i, s.kind, JSON.stringify(s.toolCalls)));
65
- console.log('--- final ---');
66
- console.log('fullContent:', JSON.stringify(result.fullContent));
67
- console.log('finalToolCalls:', JSON.stringify(result.finalToolCalls));
68
- console.log('--- last tool call has result? ---');
69
- console.log(!!result.finalToolCalls[0]?.result, '=>', result.finalToolCalls[0]?.result);
@@ -1,117 +0,0 @@
1
- // Smoke test: ToolCall rendering path + the empty-string-result regression
2
- // Simulates the React/Ink <ToolCall tool={tc}/> rendering logic in JS
3
-
4
- function simulateToolCallRender(tool) {
5
- // Mirrors src/tui/components/ToolCall.tsx exactly
6
- const hasResult = tool.result !== undefined;
7
- const isEmpty = hasResult && tool.result === '';
8
- const truncatedLines = tool.result?.split('\n').length || 0;
9
- const maxLines = 8;
10
- const truncated = hasResult && !isEmpty && truncatedLines > maxLines;
11
- const visible = isEmpty ? '' :
12
- truncated ? tool.result.split('\n').slice(0, maxLines).join('\n') :
13
- hasResult ? tool.result : null;
14
-
15
- const rendered = {
16
- header: `⚙ ${tool.name}${tool.args ? ' ' + tool.args.slice(0, 60) : ''}`,
17
- pending: visible === null,
18
- noOutput: isEmpty,
19
- lines: visible === null ? [] : visible.split('\n'),
20
- truncatedMsg: truncated ? `(+${truncatedLines - maxLines} more lines)` : null,
21
- };
22
- return rendered;
23
- }
24
-
25
- let pass = 0, fail = 0;
26
- const check = (name, cond) => {
27
- console.log((cond ? '✅' : '❌') + ' ' + name);
28
- cond ? pass++ : fail++;
29
- };
30
-
31
- // Case A: normal shell result (date output)
32
- {
33
- const r = simulateToolCallRender({
34
- name: 'shell', args: '{"command":"date"}',
35
- result: 'Mon Jul 7 10:00:00 CST 2026', isError: false,
36
- });
37
- check('A.header includes "shell"', r.header.includes('shell'));
38
- check('A.header includes date command', r.header.includes('date'));
39
- check('A.not pending', !r.pending);
40
- check('A.has 1 line', r.lines.length === 1);
41
- check('A.line shows time', r.lines[0].includes('CST'));
42
- check('A.not truncated', r.truncatedMsg === null);
43
- }
44
-
45
- // Case B: empty-string result (the bug we just fixed!)
46
- {
47
- const r = simulateToolCallRender({
48
- name: 'shell', args: '{"command":"echo > /dev/null"}',
49
- result: '', isError: false,
50
- });
51
- check('B.not pending (result IS present, just empty)', !r.pending);
52
- check('B.marked as noOutput', r.noOutput);
53
- }
54
-
55
- // Case C: pending state (no result yet)
56
- {
57
- const r = simulateToolCallRender({
58
- name: 'shell', args: '{"command":"sleep 10"}',
59
- result: undefined, isError: false,
60
- });
61
- check('C.is pending', r.pending);
62
- }
63
-
64
- // Case D: long output (>8 lines) gets truncated with explicit count
65
- {
66
- const longResult = Array.from({length: 50}, (_, i) => `line ${i+1}`).join('\n');
67
- const r = simulateToolCallRender({
68
- name: 'shell', args: '{"command":"ls"}',
69
- result: longResult, isError: false,
70
- });
71
- check('D.truncated', r.truncatedMsg !== null);
72
- check('D.truncated count = 42', r.truncatedMsg.includes('42'));
73
- check('D.shows 8 visible lines', r.lines.length === 8);
74
- }
75
-
76
- // Case E: error result
77
- {
78
- const r = simulateToolCallRender({
79
- name: 'shell', args: '{"command":"false"}',
80
- result: undefined, isError: true, // errors come via `error` field, not `result`
81
- error: 'Command failed with exit code 1',
82
- });
83
- // simulateToolCallRender doesn't read `error` — that path uses result=error string
84
- }
85
-
86
- // Case E2: error as result string
87
- {
88
- const r = simulateToolCallRender({
89
- name: 'shell', args: '{"command":"false"}',
90
- result: 'Command failed with exit code 1', isError: true,
91
- });
92
- check('E2.error message visible', r.lines.some(l => l.includes('Command failed')));
93
- check('E2.not truncated', r.truncatedMsg === null);
94
- }
95
-
96
- // Case F: multi-line output (small enough to not truncate)
97
- {
98
- const r = simulateToolCallRender({
99
- name: 'shell', args: '{"command":"pwd && ls"}',
100
- result: '/home/user\nfile1.txt\nfile2.txt', isError: false,
101
- });
102
- check('F.has 3 lines', r.lines.length === 3);
103
- check('F.line 1 is path', r.lines[0] === '/home/user');
104
- }
105
-
106
- // Case G: result with 200+ chars — user-paste style tool summary fallback test
107
- {
108
- const r = simulateToolCallRender({
109
- name: 'shell', args: '{"command":"cat big-file.txt"}',
110
- result: 'A'.repeat(500), isError: false,
111
- });
112
- check('G.shown in full (under 8 lines)', r.truncatedMsg === null);
113
- check('G.500 chars visible', r.lines[0].length === 500);
114
- }
115
-
116
- console.log(`\n=== ${pass} pass / ${fail} fail ===`);
117
- if (fail > 0) process.exit(1);
package/tsconfig.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "node16",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "declaration": true,
14
- "declarationMap": true,
15
- "sourceMap": true,
16
- "jsx": "react-jsx",
17
- "jsxImportSource": "react"
18
- },
19
- "include": ["src/**/*"],
20
- "exclude": ["node_modules", "dist"]
21
- }