thatgfsj-code 1.0.2 → 2.2.9
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.
- package/.nwt/meta.json +5 -0
- package/CHANGELOG.md +244 -0
- package/DEVELOPMENT.md +286 -0
- package/README.md +23 -1
- package/dist/app/index.d.ts +14 -0
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +21 -5
- package/dist/app/index.js.map +1 -1
- package/dist/cmd/index.d.ts +21 -0
- package/dist/cmd/index.d.ts.map +1 -1
- package/dist/cmd/index.js +144 -12
- package/dist/cmd/index.js.map +1 -1
- package/dist/session/index.d.ts +11 -0
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +83 -1
- package/dist/session/index.js.map +1 -1
- package/dist/tools/nwt.d.ts +14 -4
- package/dist/tools/nwt.d.ts.map +1 -1
- package/dist/tools/nwt.js +160 -13
- package/dist/tools/nwt.js.map +1 -1
- package/dist/tui/components/ChatList.d.ts.map +1 -1
- package/dist/tui/components/ChatList.js +4 -3
- package/dist/tui/components/ChatList.js.map +1 -1
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +2 -1
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts +1 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +9 -4
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +47 -6
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +21 -0
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +3 -2
- package/dist/tui/welcome.js.map +1 -1
- package/dist/utils/thinking.d.ts +59 -0
- package/dist/utils/thinking.d.ts.map +1 -0
- package/dist/utils/thinking.js +107 -0
- package/dist/utils/thinking.js.map +1 -0
- package/dist/version.d.ts +16 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/install.bat +63 -0
- package/install.ps1 +238 -0
- package/install.sh +113 -0
- package/package.json +5 -2
- package/src/app/index.ts +21 -5
- package/src/cmd/index.tsx +145 -13
- package/src/session/index.ts +82 -1
- package/src/tools/nwt.ts +181 -13
- package/src/tui/components/ChatList.tsx +15 -6
- package/src/tui/components/Header.tsx +2 -1
- package/src/tui/components/ToolCall.tsx +46 -10
- package/src/tui/hooks/useChat.ts +50 -6
- package/src/tui/hooks/useCommands.ts +22 -0
- package/src/tui/welcome.ts +3 -2
- package/src/utils/thinking.ts +122 -0
- package/src/version.ts +16 -0
package/src/tools/nwt.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { Tool, ToolResult } from './types.js';
|
|
10
10
|
import {
|
|
11
11
|
readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
12
|
-
readdirSync, renameSync
|
|
12
|
+
readdirSync, renameSync, unlinkSync
|
|
13
13
|
} from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
|
|
@@ -22,6 +22,7 @@ interface TimelineEvent {
|
|
|
22
22
|
files: string[];
|
|
23
23
|
tags: string[];
|
|
24
24
|
parent?: string;
|
|
25
|
+
importance: 'low' | 'normal' | 'high' | 'milestone';
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const NWT_DIR = '.nwt';
|
|
@@ -32,22 +33,41 @@ const ARCHIVE_AFTER_DAYS = 30;
|
|
|
32
33
|
|
|
33
34
|
export class NwtTool implements Tool {
|
|
34
35
|
name = 'nwt';
|
|
35
|
-
description =
|
|
36
|
+
description = `NeuroWeave Timeline - Project evolution memory.
|
|
37
|
+
Actions: init, log, history, search, story, explain, archive, diff, compact
|
|
38
|
+
|
|
39
|
+
AUTO-LOG TRIGGERS (call nwt log silently when):
|
|
40
|
+
- Created, modified, or deleted 2+ files
|
|
41
|
+
- Made an architectural decision (chose a library, pattern, or approach)
|
|
42
|
+
- Fixed a bug
|
|
43
|
+
- Added a new feature or capability
|
|
44
|
+
- Refactored existing code
|
|
45
|
+
- Set up a new project or module
|
|
46
|
+
- Resolved a security issue
|
|
47
|
+
- Completed a multi-step task
|
|
48
|
+
|
|
49
|
+
DO NOT log:
|
|
50
|
+
- Simple file reads or searches
|
|
51
|
+
- Trivial single-line changes
|
|
52
|
+
- Conversational questions with no code changes`;
|
|
36
53
|
|
|
37
54
|
inputSchema = {
|
|
38
55
|
type: 'object' as const,
|
|
39
56
|
properties: {
|
|
40
57
|
action: {
|
|
41
58
|
type: 'string',
|
|
42
|
-
description: 'Action: init, log, history, search, story, explain, archive',
|
|
59
|
+
description: 'Action: init, log, history, search, story, explain, archive, diff, compact',
|
|
43
60
|
},
|
|
44
61
|
task: { type: 'string', description: 'Short imperative title (for log)' },
|
|
45
62
|
summary: { type: 'string', description: 'What was done (for log)' },
|
|
46
63
|
reason: { type: 'string', description: 'Why it was done (for log)' },
|
|
47
64
|
files: { type: 'string', description: 'Comma-separated file paths (for log)' },
|
|
48
65
|
tags: { type: 'string', description: 'Comma-separated tags (for log)' },
|
|
66
|
+
importance: { type: 'string', description: 'Importance: low, normal, high, milestone (for log)' },
|
|
49
67
|
query: { type: 'string', description: 'Search query (for search/explain)' },
|
|
50
68
|
limit: { type: 'number', description: 'Max results (for history/search)' },
|
|
69
|
+
from_id: { type: 'string', description: 'Start event ID (for diff)' },
|
|
70
|
+
to_id: { type: 'string', description: 'End event ID (for diff)' },
|
|
51
71
|
},
|
|
52
72
|
required: ['action'],
|
|
53
73
|
};
|
|
@@ -64,8 +84,11 @@ export class NwtTool implements Tool {
|
|
|
64
84
|
{ name: 'reason', type: 'string', description: 'Why it was done', required: false },
|
|
65
85
|
{ name: 'files', type: 'string', description: 'Comma-separated file paths', required: false },
|
|
66
86
|
{ name: 'tags', type: 'string', description: 'Comma-separated tags', required: false },
|
|
87
|
+
{ name: 'importance', type: 'string', description: 'low/normal/high/milestone', required: false },
|
|
67
88
|
{ name: 'query', type: 'string', description: 'Search query', required: false },
|
|
68
89
|
{ name: 'limit', type: 'number', description: 'Max results', required: false },
|
|
90
|
+
{ name: 'from_id', type: 'string', description: 'Start event ID for diff', required: false },
|
|
91
|
+
{ name: 'to_id', type: 'string', description: 'End event ID for diff', required: false },
|
|
69
92
|
];
|
|
70
93
|
|
|
71
94
|
async execute(params: Record<string, any>, ctx?: any): Promise<ToolResult> {
|
|
@@ -88,6 +111,10 @@ export class NwtTool implements Tool {
|
|
|
88
111
|
return this.explain(cwd, params.query || '');
|
|
89
112
|
case 'archive':
|
|
90
113
|
return this.archive(cwd);
|
|
114
|
+
case 'diff':
|
|
115
|
+
return this.diff(cwd, params.from_id || '', params.to_id || '');
|
|
116
|
+
case 'compact':
|
|
117
|
+
return this.compact(cwd);
|
|
91
118
|
default:
|
|
92
119
|
return { success: false, error: `Unknown action: ${action}` };
|
|
93
120
|
}
|
|
@@ -112,7 +139,7 @@ export class NwtTool implements Tool {
|
|
|
112
139
|
|
|
113
140
|
// Write metadata
|
|
114
141
|
const meta = {
|
|
115
|
-
version: '
|
|
142
|
+
version: '2.0.0',
|
|
116
143
|
created: new Date().toISOString(),
|
|
117
144
|
description: 'NeuroWeave Timeline - Project evolution memory',
|
|
118
145
|
};
|
|
@@ -129,7 +156,7 @@ export class NwtTool implements Tool {
|
|
|
129
156
|
this.init(cwd);
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
const { task, summary, reason, files, tags } = params;
|
|
159
|
+
const { task, summary, reason, files, tags, importance } = params;
|
|
133
160
|
if (!task || !summary) {
|
|
134
161
|
return { success: false, error: 'task and summary are required' };
|
|
135
162
|
}
|
|
@@ -146,6 +173,10 @@ export class NwtTool implements Tool {
|
|
|
146
173
|
? existing.sort().pop()?.replace('.json', '')
|
|
147
174
|
: undefined;
|
|
148
175
|
|
|
176
|
+
// Validate importance
|
|
177
|
+
const validImportance = ['low', 'normal', 'high', 'milestone'];
|
|
178
|
+
const eventImportance = validImportance.includes(importance) ? importance : 'normal';
|
|
179
|
+
|
|
149
180
|
const event: TimelineEvent = {
|
|
150
181
|
id: nextId,
|
|
151
182
|
timestamp: new Date().toISOString(),
|
|
@@ -155,6 +186,7 @@ export class NwtTool implements Tool {
|
|
|
155
186
|
files: files ? files.split(',').map((f: string) => f.trim()).filter(Boolean) : [],
|
|
156
187
|
tags: tags ? tags.split(',').map((t: string) => t.trim().toLowerCase()).filter(Boolean) : [],
|
|
157
188
|
parent: parent || undefined,
|
|
189
|
+
importance: eventImportance as TimelineEvent['importance'],
|
|
158
190
|
};
|
|
159
191
|
|
|
160
192
|
writeFileSync(
|
|
@@ -186,7 +218,8 @@ export class NwtTool implements Tool {
|
|
|
186
218
|
const time = e.timestamp.split('T')[0];
|
|
187
219
|
const files = e.files.length > 0 ? ` [${e.files.join(', ')}]` : '';
|
|
188
220
|
const tags = e.tags.length > 0 ? ` {${e.tags.join(', ')}}` : '';
|
|
189
|
-
|
|
221
|
+
const imp = e.importance && e.importance !== 'normal' ? ` (${e.importance})` : '';
|
|
222
|
+
return `[${e.id}] ${time} ${e.task}${imp}${files}${tags}`;
|
|
190
223
|
});
|
|
191
224
|
|
|
192
225
|
return { success: true, output: lines.join('\n') };
|
|
@@ -227,8 +260,9 @@ export class NwtTool implements Tool {
|
|
|
227
260
|
return { success: true, output: 'No events yet.' };
|
|
228
261
|
}
|
|
229
262
|
|
|
230
|
-
// Group by
|
|
231
|
-
const milestones = events.filter(e => e.tags.includes('milestone'));
|
|
263
|
+
// Group by importance
|
|
264
|
+
const milestones = events.filter(e => e.importance === 'milestone' || e.tags.includes('milestone'));
|
|
265
|
+
const highImportance = events.filter(e => e.importance === 'high');
|
|
232
266
|
const decisions = events.filter(e => e.tags.includes('decision'));
|
|
233
267
|
const totalFiles = new Set(events.flatMap(e => e.files)).size;
|
|
234
268
|
|
|
@@ -245,6 +279,14 @@ export class NwtTool implements Tool {
|
|
|
245
279
|
lines.push('');
|
|
246
280
|
}
|
|
247
281
|
|
|
282
|
+
if (highImportance.length > 0) {
|
|
283
|
+
lines.push('High Importance:');
|
|
284
|
+
for (const h of highImportance.slice(-5)) {
|
|
285
|
+
lines.push(` [${h.id}] ${h.task} - ${h.summary}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push('');
|
|
288
|
+
}
|
|
289
|
+
|
|
248
290
|
if (decisions.length > 0) {
|
|
249
291
|
lines.push('Key Decisions:');
|
|
250
292
|
for (const d of decisions.slice(-5)) {
|
|
@@ -256,7 +298,8 @@ export class NwtTool implements Tool {
|
|
|
256
298
|
lines.push('Recent Activity:');
|
|
257
299
|
for (const e of events.slice(-5)) {
|
|
258
300
|
const time = e.timestamp.split('T')[0];
|
|
259
|
-
|
|
301
|
+
const imp = e.importance && e.importance !== 'normal' ? ` (${e.importance})` : '';
|
|
302
|
+
lines.push(` ${time} ${e.task}${imp}`);
|
|
260
303
|
}
|
|
261
304
|
|
|
262
305
|
return { success: true, output: lines.join('\n') };
|
|
@@ -284,6 +327,135 @@ export class NwtTool implements Tool {
|
|
|
284
327
|
return { success: true, output: `History of ${filePath}:\n\n${lines.join('\n\n')}` };
|
|
285
328
|
}
|
|
286
329
|
|
|
330
|
+
// ── Diff ──────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
private diff(cwd: string, fromId: string, toId: string): ToolResult {
|
|
333
|
+
if (!fromId || !toId) {
|
|
334
|
+
return { success: false, error: 'from_id and to_id are required' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const events = this.loadEvents(cwd);
|
|
338
|
+
const fromEvent = events.find(e => e.id === fromId.padStart(6, '0'));
|
|
339
|
+
const toEvent = events.find(e => e.id === toId.padStart(6, '0'));
|
|
340
|
+
|
|
341
|
+
if (!fromEvent) return { success: false, error: `Event ${fromId} not found` };
|
|
342
|
+
if (!toEvent) return { success: false, error: `Event ${toId} not found` };
|
|
343
|
+
|
|
344
|
+
// Get events between from and to
|
|
345
|
+
const fromIdx = events.indexOf(fromEvent);
|
|
346
|
+
const toIdx = events.indexOf(toEvent);
|
|
347
|
+
const between = events.slice(fromIdx, toIdx + 1);
|
|
348
|
+
|
|
349
|
+
// Collect all files touched
|
|
350
|
+
const allFiles = new Set(between.flatMap(e => e.files));
|
|
351
|
+
const fromFiles = new Set(fromEvent.files);
|
|
352
|
+
const toFiles = new Set(toEvent.files);
|
|
353
|
+
|
|
354
|
+
// Files added/removed/modified
|
|
355
|
+
const added = [...toFiles].filter(f => !fromFiles.has(f));
|
|
356
|
+
const removed = [...fromFiles].filter(f => !toFiles.has(f));
|
|
357
|
+
const modified = [...allFiles].filter(f => fromFiles.has(f) && toFiles.has(f));
|
|
358
|
+
|
|
359
|
+
const lines = [
|
|
360
|
+
`Diff: [${fromEvent.id}] → [${toEvent.id}]`,
|
|
361
|
+
`Events: ${between.length} between these points`,
|
|
362
|
+
'',
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
if (added.length > 0) lines.push(`Added: ${added.join(', ')}`);
|
|
366
|
+
if (removed.length > 0) lines.push(`Removed: ${removed.join(', ')}`);
|
|
367
|
+
if (modified.length > 0) lines.push(`Modified: ${modified.join(', ')}`);
|
|
368
|
+
|
|
369
|
+
lines.push('', 'Events in range:');
|
|
370
|
+
for (const e of between) {
|
|
371
|
+
const time = e.timestamp.split('T')[0];
|
|
372
|
+
lines.push(` [${e.id}] ${time} ${e.task}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return { success: true, output: lines.join('\n') };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Compact ───────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
private compact(cwd: string): ToolResult {
|
|
381
|
+
const events = this.loadEvents(cwd);
|
|
382
|
+
if (events.length < 10) {
|
|
383
|
+
return { success: true, output: 'Not enough events to compact (need 10+).' };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const nwtDir = join(cwd, NWT_DIR);
|
|
387
|
+
const eventsDir = join(nwtDir, EVENTS_DIR);
|
|
388
|
+
|
|
389
|
+
// Group consecutive events with same tags
|
|
390
|
+
const groups: TimelineEvent[][] = [];
|
|
391
|
+
let currentGroup: TimelineEvent[] = [events[0]];
|
|
392
|
+
|
|
393
|
+
for (let i = 1; i < events.length; i++) {
|
|
394
|
+
const prev = events[i - 1];
|
|
395
|
+
const curr = events[i];
|
|
396
|
+
|
|
397
|
+
// Same tags and close in time (within 1 hour)
|
|
398
|
+
const sameTags = curr.tags.join(',') === prev.tags.join(',');
|
|
399
|
+
const timeDiff = new Date(curr.timestamp).getTime() - new Date(prev.timestamp).getTime();
|
|
400
|
+
const closeInTime = timeDiff < 3600000; // 1 hour
|
|
401
|
+
|
|
402
|
+
if (sameTags && closeInTime) {
|
|
403
|
+
currentGroup.push(curr);
|
|
404
|
+
} else {
|
|
405
|
+
groups.push(currentGroup);
|
|
406
|
+
currentGroup = [curr];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
groups.push(currentGroup);
|
|
410
|
+
|
|
411
|
+
// Merge groups with 3+ events
|
|
412
|
+
let merged = 0;
|
|
413
|
+
const newEvents: TimelineEvent[] = [];
|
|
414
|
+
|
|
415
|
+
for (const group of groups) {
|
|
416
|
+
if (group.length >= 3) {
|
|
417
|
+
// Merge into one summary event
|
|
418
|
+
const first = group[0];
|
|
419
|
+
const last = group[group.length - 1];
|
|
420
|
+
const allFiles = [...new Set(group.flatMap(e => e.files))];
|
|
421
|
+
const allTags = [...new Set(group.flatMap(e => e.tags))];
|
|
422
|
+
|
|
423
|
+
newEvents.push({
|
|
424
|
+
id: first.id,
|
|
425
|
+
timestamp: first.timestamp,
|
|
426
|
+
task: `${first.task} ... ${last.task}`,
|
|
427
|
+
summary: `Compacted ${group.length} events: ${group.map(e => e.task).join(', ')}`,
|
|
428
|
+
files: allFiles,
|
|
429
|
+
tags: allTags,
|
|
430
|
+
importance: first.importance,
|
|
431
|
+
});
|
|
432
|
+
merged += group.length - 1;
|
|
433
|
+
} else {
|
|
434
|
+
newEvents.push(...group);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (merged === 0) {
|
|
439
|
+
return { success: true, output: 'No events to compact.' };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Re-write events
|
|
443
|
+
for (const file of readdirSync(eventsDir).filter(f => f.endsWith('.json'))) {
|
|
444
|
+
unlinkSync(join(eventsDir, file));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (let i = 0; i < newEvents.length; i++) {
|
|
448
|
+
const event = newEvents[i];
|
|
449
|
+
event.id = (i + 1).toString().padStart(6, '0');
|
|
450
|
+
writeFileSync(join(eventsDir, `${event.id}.json`), JSON.stringify(event, null, 2));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
success: true,
|
|
455
|
+
output: `Compacted: ${events.length} → ${newEvents.length} events (merged ${merged})`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
287
459
|
// ── Archive ───────────────────────────────────────────
|
|
288
460
|
|
|
289
461
|
private archive(cwd: string): ToolResult {
|
|
@@ -345,10 +517,6 @@ export class NwtTool implements Tool {
|
|
|
345
517
|
|
|
346
518
|
// ── Auto Archive ──────────────────────────────────────
|
|
347
519
|
|
|
348
|
-
/**
|
|
349
|
-
* Auto-archive events older than 30 days.
|
|
350
|
-
* Runs silently on init - no output unless something was archived.
|
|
351
|
-
*/
|
|
352
520
|
private autoArchiveIfNeeded(cwd: string): void {
|
|
353
521
|
try {
|
|
354
522
|
const nwtDir = join(cwd, NWT_DIR);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
2
|
import React, { memo } from 'react';
|
|
3
|
-
import { Box } from 'ink';
|
|
3
|
+
import { Box, Static } from 'ink';
|
|
4
4
|
import { ChatMessage, type MessageData } from './ChatMessage.js';
|
|
5
5
|
import type { ToolCallData } from './ToolCall.js';
|
|
6
6
|
|
|
@@ -12,17 +12,26 @@ interface Props {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const ChatList = memo(function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
|
|
15
|
+
const hasStreaming = !!(streaming || (streamingToolCalls && streamingToolCalls.length > 0));
|
|
16
|
+
|
|
15
17
|
return (
|
|
16
18
|
<Box flexDirection="column">
|
|
17
|
-
{messages
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
{/* Completed messages - Static prevents re-rendering */}
|
|
20
|
+
{messages.length > 0 && (
|
|
21
|
+
<Static items={messages}>
|
|
22
|
+
{(msg: MessageData, index: number) => (
|
|
23
|
+
<ChatMessage key={`msg-${index}`} message={msg} width={width} />
|
|
24
|
+
)}
|
|
25
|
+
</Static>
|
|
26
|
+
)}
|
|
27
|
+
|
|
28
|
+
{/* Streaming content - only this part re-renders */}
|
|
29
|
+
{hasStreaming && (
|
|
21
30
|
<ChatMessage
|
|
22
31
|
message={{
|
|
23
32
|
role: 'assistant',
|
|
24
33
|
content: streaming || '',
|
|
25
|
-
toolCalls: streamingToolCalls,
|
|
34
|
+
toolCalls: streamingToolCalls && streamingToolCalls.length > 0 ? streamingToolCalls : undefined,
|
|
26
35
|
}}
|
|
27
36
|
width={width}
|
|
28
37
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
+
import { PRODUCT_VERSION_DISPLAY } from '../../version.js';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
provider: string;
|
|
@@ -14,7 +15,7 @@ export const Header = React.memo(function Header({ provider, model }: Props) {
|
|
|
14
15
|
<Box>
|
|
15
16
|
<Text color="#06B6D4" bold> ⚡ </Text>
|
|
16
17
|
<Text color="#22D3EE" bold>THATGFSJ CODE</Text>
|
|
17
|
-
<Text dimColor>
|
|
18
|
+
<Text dimColor> {PRODUCT_VERSION_DISPLAY}</Text>
|
|
18
19
|
</Box>
|
|
19
20
|
<Box>
|
|
20
21
|
<Text color="#06B6D4" bold> {provider} </Text>
|
|
@@ -40,9 +40,14 @@ function truncateOutput(output: string, maxLines = 8): { text: string; truncated
|
|
|
40
40
|
return { text: lines.slice(0, maxLines).join('\n'), truncated: true };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function ToolCall({ tool }: Props) {
|
|
43
|
+
export function ToolCall({ tool, width }: Props) {
|
|
44
44
|
const label = formatToolName(tool.name, tool.args);
|
|
45
|
-
|
|
45
|
+
// v2.2.7 edge: treat null same as undefined (both mean "still
|
|
46
|
+
// running"). Previous check `result !== undefined` would render
|
|
47
|
+
// null as an empty string output, causing confusion.
|
|
48
|
+
const result = (tool.result !== undefined && tool.result !== null)
|
|
49
|
+
? truncateOutput(tool.result)
|
|
50
|
+
: null;
|
|
46
51
|
|
|
47
52
|
return (
|
|
48
53
|
<Box flexDirection="column" marginBottom={0} paddingLeft={1}>
|
|
@@ -53,15 +58,46 @@ export function ToolCall({ tool }: Props) {
|
|
|
53
58
|
{label && <Text color="#64748B"> {label}</Text>}
|
|
54
59
|
</Box>
|
|
55
60
|
|
|
56
|
-
{/* Result
|
|
57
|
-
|
|
61
|
+
{/* Result — v2.2.6 fix:
|
|
62
|
+
- Use `!== undefined` (not truthy) so empty string results
|
|
63
|
+
still render (truthy check dropped them, making it look
|
|
64
|
+
like the tool result was missing).
|
|
65
|
+
- Switch wrap from "truncate" to "wrap" so long lines don't
|
|
66
|
+
silently disappear off-screen.
|
|
67
|
+
- Always render the Box even when result is empty (just
|
|
68
|
+
with a "(no output)" marker), so the visual frame stays
|
|
69
|
+
consistent.
|
|
70
|
+
- If truncated, show explicit "(+N more lines)" indicator
|
|
71
|
+
so users know the result was clipped. */}
|
|
72
|
+
{result !== null && (
|
|
58
73
|
<Box paddingLeft={2} flexDirection="column">
|
|
59
|
-
{result.text.
|
|
60
|
-
<Text
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
{result.text.length === 0 ? (
|
|
75
|
+
<Text color="#94A3B8" dimColor>(no output)</Text>
|
|
76
|
+
) : (
|
|
77
|
+
<>
|
|
78
|
+
{result.text.split('\n').map((line, i) => (
|
|
79
|
+
<Text
|
|
80
|
+
key={i}
|
|
81
|
+
color={tool.isError ? '#EF4444' : '#64748B'}
|
|
82
|
+
wrap="wrap"
|
|
83
|
+
>
|
|
84
|
+
{line || ' '}
|
|
85
|
+
</Text>
|
|
86
|
+
))}
|
|
87
|
+
{result.truncated && (
|
|
88
|
+
<Text color="#94A3B8" dimColor>
|
|
89
|
+
{' '}(+{tool.result!.split('\n').length - 8} more lines)
|
|
90
|
+
</Text>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</Box>
|
|
95
|
+
)}
|
|
96
|
+
{/* v2.2.6: if result is undefined (still running), show a
|
|
97
|
+
pending indicator so the user knows the tool is in flight. */}
|
|
98
|
+
{result === null && tool.result === undefined && (
|
|
99
|
+
<Box paddingLeft={2}>
|
|
100
|
+
<Text color="#94A3B8" dimColor> ⏳ running...</Text>
|
|
65
101
|
</Box>
|
|
66
102
|
)}
|
|
67
103
|
</Box>
|
package/src/tui/hooks/useChat.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { useState, useCallback, useRef } from 'react';
|
|
|
2
2
|
import type { MessageData } from '../components/ChatMessage.js';
|
|
3
3
|
import type { ToolCallData } from '../components/ToolCall.js';
|
|
4
4
|
import type { App } from '../../app/index.js';
|
|
5
|
+
import { compressThinking, splitThinking, summarizeThinking } from '../../utils/thinking.js';
|
|
5
6
|
|
|
6
7
|
interface ChatState {
|
|
7
8
|
messages: MessageData[];
|
|
@@ -95,20 +96,63 @@ export function useChat(app: App) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// v2.2.4 (port from v2.1.0): DO NOT persist truncated assistant
|
|
100
|
+
// messages. The previous code literally wrote `'\n\n[已中断]'`
|
|
101
|
+
// as a suffix and persisted it — which is what created the
|
|
102
|
+
// hallucination loop where the next turn's LLM echoed the
|
|
103
|
+
// marker back. The fix is two-pronged:
|
|
104
|
+
// 1. Never persist when the stream was aborted (here).
|
|
105
|
+
// 2. SessionManager.addMessageSafe drops messages that match
|
|
106
|
+
// the pollution filter as a belt-and-suspenders check
|
|
107
|
+
// for cases where we somehow persist a polluted message.
|
|
108
|
+
const wasAborted = abortRef.current;
|
|
109
|
+
const shouldPersist = !wasAborted &&
|
|
110
|
+
(fullContent.trim() || currentToolCalls.length > 0);
|
|
111
|
+
|
|
112
|
+
if (shouldPersist) {
|
|
113
|
+
// v2.2.5: strip <think> blocks from the persisted message
|
|
114
|
+
// when compression is enabled. Same rationale as in
|
|
115
|
+
// cmd/index.tsx — keeps history compact, avoids re-feeding
|
|
116
|
+
// reasoning into the next turn's context window.
|
|
117
|
+
const toPersist = compressThinking(fullContent, app.showThinking);
|
|
118
|
+
app.session.addMessageSafe('assistant', toPersist);
|
|
102
119
|
}
|
|
103
120
|
|
|
121
|
+
// v2.2.5: build a displayable version. When thinking is hidden
|
|
122
|
+
// we still want the user to see a one-line indicator of how
|
|
123
|
+
// much reasoning the model did, plus the conclusion.
|
|
124
|
+
const split = splitThinking(fullContent);
|
|
125
|
+
const displayContent = app.showThinking
|
|
126
|
+
? fullContent
|
|
127
|
+
: (split.thinking
|
|
128
|
+
? `${summarizeThinking(split)}\n${split.conclusion}`
|
|
129
|
+
: fullContent);
|
|
130
|
+
|
|
131
|
+
// v2.2.6 (tool-result belt-and-suspenders): if any tool call
|
|
132
|
+
// returned text, also append a compact "[tool: name → result]"
|
|
133
|
+
// summary to the persisted/displayed content. This guarantees
|
|
134
|
+
// the user sees the tool output regardless of whether the Ink
|
|
135
|
+
// <ToolCall/> component renders it correctly. Past sessions
|
|
136
|
+
// have had cases where the streaming ToolCall rendering failed
|
|
137
|
+
// silently (e.g. result was empty string, wrap=truncate cut
|
|
138
|
+
// long output off-screen) and the user had no idea what the
|
|
139
|
+
// tool actually returned.
|
|
140
|
+
const toolSummary = currentToolCalls.length > 0
|
|
141
|
+
? '\n\n' + currentToolCalls.map((tc) => {
|
|
142
|
+
const r = tc.result !== undefined ? tc.result : '(no result)';
|
|
143
|
+
const short = r.length > 200 ? r.slice(0, 197) + '...' : r;
|
|
144
|
+
return `[tool: ${tc.name} → ${short}]`;
|
|
145
|
+
}).join('\n')
|
|
146
|
+
: '';
|
|
147
|
+
|
|
104
148
|
setState(prev => ({
|
|
105
149
|
...prev,
|
|
106
150
|
messages: [
|
|
107
151
|
...prev.messages,
|
|
108
|
-
...(
|
|
152
|
+
...(shouldPersist
|
|
109
153
|
? [{
|
|
110
154
|
role: 'assistant' as const,
|
|
111
|
-
content:
|
|
155
|
+
content: displayContent + toolSummary,
|
|
112
156
|
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
113
157
|
}]
|
|
114
158
|
: []),
|
|
@@ -18,6 +18,7 @@ const CMD_ALIASES: Record<string, string> = {
|
|
|
18
18
|
'/mcp': '/mcp',
|
|
19
19
|
'/帮助': '/help',
|
|
20
20
|
'/服务商': '/provider',
|
|
21
|
+
'/思考': '/thinking',
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export const COMMAND_LIST = [
|
|
@@ -125,6 +126,26 @@ export function useCommands(app: App) {
|
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// ── /thinking on|off ─────────────────────────────────
|
|
130
|
+
if (name === '/thinking' || name === '/思考') {
|
|
131
|
+
if (!arg) {
|
|
132
|
+
return {
|
|
133
|
+
handled: true,
|
|
134
|
+
output: `思考块显示: ${app.showThinking ? '开启 (显示完整 <think>...</think>)' : '关闭 (压缩为单行提示)'}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const want = arg.toLowerCase();
|
|
138
|
+
if (want === 'on' || want === '开启' || want === 'true' || want === '1') {
|
|
139
|
+
app.showThinking = true;
|
|
140
|
+
return { handled: true, output: '✓ 已开启完整思考块显示' };
|
|
141
|
+
}
|
|
142
|
+
if (want === 'off' || want === '关闭' || want === 'false' || want === '0') {
|
|
143
|
+
app.showThinking = false;
|
|
144
|
+
return { handled: true, output: '✓ 已关闭思考块显示 (压缩为单行提示)' };
|
|
145
|
+
}
|
|
146
|
+
return { handled: true, output: `用法: /thinking on|off (当前: ${app.showThinking ? 'on' : 'off'})` };
|
|
147
|
+
}
|
|
148
|
+
|
|
128
149
|
// ── /help ───────────────────────────────────────────
|
|
129
150
|
if (name === '/help') {
|
|
130
151
|
return {
|
|
@@ -135,6 +156,7 @@ export function useCommands(app: App) {
|
|
|
135
156
|
' /服务商 更换服务商',
|
|
136
157
|
' /新建 新建会话',
|
|
137
158
|
' /压缩 压缩上下文',
|
|
159
|
+
' /思考 [on|off] 切换思考块显示',
|
|
138
160
|
' /技能 [id] 管理技能',
|
|
139
161
|
' /mcp MCP 设置',
|
|
140
162
|
' /帮助 查看帮助',
|
package/src/tui/welcome.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { PROVIDERS, getModelsForProvider, listProviders, isCustomProvider } from '../config/providers.js';
|
|
11
|
+
import { PRODUCT_VERSION_DISPLAY } from '../version.js';
|
|
11
12
|
import type { ProviderName } from '../config/types.js';
|
|
12
13
|
|
|
13
14
|
const line = chalk.gray('─'.repeat(52));
|
|
@@ -18,7 +19,7 @@ export class WelcomeScreen {
|
|
|
18
19
|
if (hasApiKey) return;
|
|
19
20
|
|
|
20
21
|
console.log();
|
|
21
|
-
console.log(chalk.cyan.bold(' ⚡ Thatgfsj Code') + chalk.gray('
|
|
22
|
+
console.log(chalk.cyan.bold(' ⚡ Thatgfsj Code') + chalk.gray(' ' + PRODUCT_VERSION_DISPLAY));
|
|
22
23
|
console.log(chalk.gray(' AI Coding Assistant'));
|
|
23
24
|
console.log(line);
|
|
24
25
|
console.log();
|
|
@@ -35,7 +36,7 @@ export class WelcomeScreen {
|
|
|
35
36
|
static async interactiveSetup(): Promise<void> {
|
|
36
37
|
console.clear();
|
|
37
38
|
console.log();
|
|
38
|
-
console.log(chalk.cyan.bold(' ⚡ Thatgfsj Code') + chalk.gray(' - Setup'));
|
|
39
|
+
console.log(chalk.cyan.bold(' ⚡ Thatgfsj Code') + chalk.gray(' ' + PRODUCT_VERSION_DISPLAY + ' - Setup'));
|
|
39
40
|
console.log(line);
|
|
40
41
|
console.log();
|
|
41
42
|
|