thatgfsj-code 0.6.0 → 0.7.1

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 (149) hide show
  1. package/dist/app/index.d.ts +2 -0
  2. package/dist/app/index.d.ts.map +1 -1
  3. package/dist/app/index.js +10 -5
  4. package/dist/app/index.js.map +1 -1
  5. package/dist/cmd/index.js +1 -1
  6. package/dist/prompts/index.d.ts +2 -0
  7. package/dist/prompts/index.d.ts.map +1 -1
  8. package/dist/prompts/index.js +7 -0
  9. package/dist/prompts/index.js.map +1 -1
  10. package/dist/skills/brainstorming.d.ts +3 -0
  11. package/dist/skills/brainstorming.d.ts.map +1 -0
  12. package/dist/skills/brainstorming.js +42 -0
  13. package/dist/skills/brainstorming.js.map +1 -0
  14. package/dist/skills/code-review.d.ts +3 -0
  15. package/dist/skills/code-review.d.ts.map +1 -0
  16. package/dist/skills/code-review.js +44 -0
  17. package/dist/skills/code-review.js.map +1 -0
  18. package/dist/skills/executing-plans.d.ts +3 -0
  19. package/dist/skills/executing-plans.d.ts.map +1 -0
  20. package/dist/skills/executing-plans.js +26 -0
  21. package/dist/skills/executing-plans.js.map +1 -0
  22. package/dist/skills/frontend-design.d.ts +3 -0
  23. package/dist/skills/frontend-design.d.ts.map +1 -0
  24. package/dist/skills/frontend-design.js +34 -0
  25. package/dist/skills/frontend-design.js.map +1 -0
  26. package/dist/skills/git-workflow.d.ts +3 -0
  27. package/dist/skills/git-workflow.d.ts.map +1 -0
  28. package/dist/skills/git-workflow.js +35 -0
  29. package/dist/skills/git-workflow.js.map +1 -0
  30. package/dist/skills/improve-architecture.d.ts +3 -0
  31. package/dist/skills/improve-architecture.d.ts.map +1 -0
  32. package/dist/skills/improve-architecture.js +37 -0
  33. package/dist/skills/improve-architecture.js.map +1 -0
  34. package/dist/skills/index.d.ts +50 -0
  35. package/dist/skills/index.d.ts.map +1 -0
  36. package/dist/skills/index.js +98 -0
  37. package/dist/skills/index.js.map +1 -0
  38. package/dist/skills/neuroweave.d.ts +3 -0
  39. package/dist/skills/neuroweave.d.ts.map +1 -0
  40. package/dist/skills/neuroweave.js +46 -0
  41. package/dist/skills/neuroweave.js.map +1 -0
  42. package/dist/skills/playwright.d.ts +3 -0
  43. package/dist/skills/playwright.d.ts.map +1 -0
  44. package/dist/skills/playwright.js +71 -0
  45. package/dist/skills/playwright.js.map +1 -0
  46. package/dist/skills/prototype.d.ts +3 -0
  47. package/dist/skills/prototype.d.ts.map +1 -0
  48. package/dist/skills/prototype.js +29 -0
  49. package/dist/skills/prototype.js.map +1 -0
  50. package/dist/skills/subagent.d.ts +3 -0
  51. package/dist/skills/subagent.d.ts.map +1 -0
  52. package/dist/skills/subagent.js +27 -0
  53. package/dist/skills/subagent.js.map +1 -0
  54. package/dist/skills/supabase.d.ts +3 -0
  55. package/dist/skills/supabase.d.ts.map +1 -0
  56. package/dist/skills/supabase.js +43 -0
  57. package/dist/skills/supabase.js.map +1 -0
  58. package/dist/skills/systematic-debugging.d.ts +3 -0
  59. package/dist/skills/systematic-debugging.d.ts.map +1 -0
  60. package/dist/skills/systematic-debugging.js +43 -0
  61. package/dist/skills/systematic-debugging.js.map +1 -0
  62. package/dist/skills/tdd.d.ts +3 -0
  63. package/dist/skills/tdd.d.ts.map +1 -0
  64. package/dist/skills/tdd.js +38 -0
  65. package/dist/skills/tdd.js.map +1 -0
  66. package/dist/skills/triage.d.ts +3 -0
  67. package/dist/skills/triage.d.ts.map +1 -0
  68. package/dist/skills/triage.js +34 -0
  69. package/dist/skills/triage.js.map +1 -0
  70. package/dist/skills/verification.d.ts +3 -0
  71. package/dist/skills/verification.d.ts.map +1 -0
  72. package/dist/skills/verification.js +30 -0
  73. package/dist/skills/verification.js.map +1 -0
  74. package/dist/skills/writing-plans.d.ts +3 -0
  75. package/dist/skills/writing-plans.d.ts.map +1 -0
  76. package/dist/skills/writing-plans.js +41 -0
  77. package/dist/skills/writing-plans.js.map +1 -0
  78. package/dist/tools/index.d.ts +1 -0
  79. package/dist/tools/index.d.ts.map +1 -1
  80. package/dist/tools/index.js +3 -0
  81. package/dist/tools/index.js.map +1 -1
  82. package/dist/tools/nwt.d.ts +70 -0
  83. package/dist/tools/nwt.d.ts.map +1 -0
  84. package/dist/tools/nwt.js +289 -0
  85. package/dist/tools/nwt.js.map +1 -0
  86. package/dist/tui/app.d.ts.map +1 -1
  87. package/dist/tui/app.js +6 -6
  88. package/dist/tui/app.js.map +1 -1
  89. package/dist/tui/components/ChatList.d.ts +4 -5
  90. package/dist/tui/components/ChatList.d.ts.map +1 -1
  91. package/dist/tui/components/ChatList.js +6 -4
  92. package/dist/tui/components/ChatList.js.map +1 -1
  93. package/dist/tui/components/ChatMessage.d.ts +2 -1
  94. package/dist/tui/components/ChatMessage.d.ts.map +1 -1
  95. package/dist/tui/components/ChatMessage.js +10 -8
  96. package/dist/tui/components/ChatMessage.js.map +1 -1
  97. package/dist/tui/components/Header.d.ts +1 -1
  98. package/dist/tui/components/Header.d.ts.map +1 -1
  99. package/dist/tui/components/Header.js +5 -3
  100. package/dist/tui/components/Header.js.map +1 -1
  101. package/dist/tui/components/StatusBar.d.ts +9 -0
  102. package/dist/tui/components/StatusBar.d.ts.map +1 -0
  103. package/dist/tui/components/StatusBar.js +10 -0
  104. package/dist/tui/components/StatusBar.js.map +1 -0
  105. package/dist/tui/components/Thinking.js +1 -1
  106. package/dist/tui/components/Thinking.js.map +1 -1
  107. package/dist/tui/components/ToolCall.d.ts +2 -1
  108. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  109. package/dist/tui/components/ToolCall.js +29 -24
  110. package/dist/tui/components/ToolCall.js.map +1 -1
  111. package/dist/tui/components/UserInput.d.ts +1 -1
  112. package/dist/tui/components/UserInput.d.ts.map +1 -1
  113. package/dist/tui/components/UserInput.js +19 -42
  114. package/dist/tui/components/UserInput.js.map +1 -1
  115. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  116. package/dist/tui/hooks/useCommands.js +28 -1
  117. package/dist/tui/hooks/useCommands.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/app/index.ts +12 -6
  120. package/src/cmd/index.tsx +1 -1
  121. package/src/prompts/index.ts +8 -0
  122. package/src/skills/brainstorming.ts +43 -0
  123. package/src/skills/code-review.ts +45 -0
  124. package/src/skills/executing-plans.ts +27 -0
  125. package/src/skills/frontend-design.ts +35 -0
  126. package/src/skills/git-workflow.ts +36 -0
  127. package/src/skills/improve-architecture.ts +38 -0
  128. package/src/skills/index.ts +136 -0
  129. package/src/skills/neuroweave.ts +47 -0
  130. package/src/skills/playwright.ts +72 -0
  131. package/src/skills/prototype.ts +30 -0
  132. package/src/skills/subagent.ts +28 -0
  133. package/src/skills/supabase.ts +44 -0
  134. package/src/skills/systematic-debugging.ts +44 -0
  135. package/src/skills/tdd.ts +39 -0
  136. package/src/skills/triage.ts +35 -0
  137. package/src/skills/verification.ts +31 -0
  138. package/src/skills/writing-plans.ts +42 -0
  139. package/src/tools/index.ts +3 -0
  140. package/src/tools/nwt.ts +362 -0
  141. package/src/tui/app.tsx +9 -8
  142. package/src/tui/components/ChatList.tsx +8 -5
  143. package/src/tui/components/ChatMessage.tsx +16 -13
  144. package/src/tui/components/Header.tsx +15 -9
  145. package/src/tui/components/StatusBar.tsx +30 -0
  146. package/src/tui/components/Thinking.tsx +3 -3
  147. package/src/tui/components/ToolCall.tsx +34 -32
  148. package/src/tui/components/UserInput.tsx +20 -54
  149. package/src/tui/hooks/useCommands.ts +29 -1
@@ -0,0 +1,362 @@
1
+ /**
2
+ * NeuroWeave Timeline (NWT) Tool - Project evolution memory
3
+ * Built-in port of https://github.com/Thatgfsj/neuroweave-timeline
4
+ *
5
+ * Stores meaningful actions (decisions, refactors, bug fixes) as timeline events
6
+ * in .nwt/ directory. Supports monthly archives (max 1 month per archive).
7
+ */
8
+
9
+ import type { Tool, ToolResult } from './types.js';
10
+ import {
11
+ readFileSync, writeFileSync, existsSync, mkdirSync,
12
+ readdirSync, renameSync
13
+ } from 'fs';
14
+ import { join } from 'path';
15
+
16
+ interface TimelineEvent {
17
+ id: string;
18
+ timestamp: string;
19
+ task: string;
20
+ summary: string;
21
+ reason?: string;
22
+ files: string[];
23
+ tags: string[];
24
+ parent?: string;
25
+ }
26
+
27
+ const NWT_DIR = '.nwt';
28
+ const EVENTS_DIR = 'events';
29
+ const ARCHIVE_DIR = 'archives';
30
+ const MAX_EVENTS = 500;
31
+ const ARCHIVE_AFTER_DAYS = 30;
32
+
33
+ export class NwtTool implements Tool {
34
+ name = 'nwt';
35
+ description = 'NeuroWeave Timeline - Project evolution memory. Log decisions, track changes, search history. Actions: init, log, history, search, story, explain, archive';
36
+
37
+ inputSchema = {
38
+ type: 'object' as const,
39
+ properties: {
40
+ action: {
41
+ type: 'string',
42
+ description: 'Action: init, log, history, search, story, explain, archive',
43
+ },
44
+ task: { type: 'string', description: 'Short imperative title (for log)' },
45
+ summary: { type: 'string', description: 'What was done (for log)' },
46
+ reason: { type: 'string', description: 'Why it was done (for log)' },
47
+ files: { type: 'string', description: 'Comma-separated file paths (for log)' },
48
+ tags: { type: 'string', description: 'Comma-separated tags (for log)' },
49
+ query: { type: 'string', description: 'Search query (for search/explain)' },
50
+ limit: { type: 'number', description: 'Max results (for history/search)' },
51
+ },
52
+ required: ['action'],
53
+ };
54
+
55
+ metadata = {
56
+ permissions: ['read', 'write'] as ('read' | 'write' | 'execute' | 'network')[],
57
+ tags: ['nwt', 'timeline', 'memory', 'history'],
58
+ };
59
+
60
+ parameters = [
61
+ { name: 'action', type: 'string', description: 'Action to perform', required: true },
62
+ { name: 'task', type: 'string', description: 'Short title', required: false },
63
+ { name: 'summary', type: 'string', description: 'What was done', required: false },
64
+ { name: 'reason', type: 'string', description: 'Why it was done', required: false },
65
+ { name: 'files', type: 'string', description: 'Comma-separated file paths', required: false },
66
+ { name: 'tags', type: 'string', description: 'Comma-separated tags', required: false },
67
+ { name: 'query', type: 'string', description: 'Search query', required: false },
68
+ { name: 'limit', type: 'number', description: 'Max results', required: false },
69
+ ];
70
+
71
+ async execute(params: Record<string, any>, ctx?: any): Promise<ToolResult> {
72
+ const cwd = ctx?.workingDirectory || process.cwd();
73
+ const action = params.action;
74
+
75
+ try {
76
+ switch (action) {
77
+ case 'init':
78
+ return this.init(cwd);
79
+ case 'log':
80
+ return this.log(cwd, params);
81
+ case 'history':
82
+ return this.history(cwd, params.limit || 20);
83
+ case 'search':
84
+ return this.search(cwd, params.query || '', params.limit || 20);
85
+ case 'story':
86
+ return this.story(cwd);
87
+ case 'explain':
88
+ return this.explain(cwd, params.query || '');
89
+ case 'archive':
90
+ return this.archive(cwd);
91
+ default:
92
+ return { success: false, error: `Unknown action: ${action}` };
93
+ }
94
+ } catch (error: any) {
95
+ return { success: false, error: error.message };
96
+ }
97
+ }
98
+
99
+ // ── Init ──────────────────────────────────────────────
100
+
101
+ private init(cwd: string): ToolResult {
102
+ const nwtDir = join(cwd, NWT_DIR);
103
+ const eventsDir = join(nwtDir, EVENTS_DIR);
104
+ const archiveDir = join(nwtDir, ARCHIVE_DIR);
105
+
106
+ if (!existsSync(nwtDir)) mkdirSync(nwtDir, { recursive: true });
107
+ if (!existsSync(eventsDir)) mkdirSync(eventsDir, { recursive: true });
108
+ if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
109
+
110
+ // Write metadata
111
+ const meta = {
112
+ version: '1.0.0',
113
+ created: new Date().toISOString(),
114
+ description: 'NeuroWeave Timeline - Project evolution memory',
115
+ };
116
+ writeFileSync(join(nwtDir, 'meta.json'), JSON.stringify(meta, null, 2));
117
+
118
+ return { success: true, output: 'NWT initialized in .nwt/' };
119
+ }
120
+
121
+ // ── Log Event ─────────────────────────────────────────
122
+
123
+ private log(cwd: string, params: Record<string, any>): ToolResult {
124
+ const nwtDir = join(cwd, NWT_DIR);
125
+ if (!existsSync(nwtDir)) {
126
+ this.init(cwd);
127
+ }
128
+
129
+ const { task, summary, reason, files, tags } = params;
130
+ if (!task || !summary) {
131
+ return { success: false, error: 'task and summary are required' };
132
+ }
133
+
134
+ // Get next ID
135
+ const eventsDir = join(nwtDir, EVENTS_DIR);
136
+ const existing = existsSync(eventsDir)
137
+ ? readdirSync(eventsDir).filter(f => f.endsWith('.json'))
138
+ : [];
139
+ const nextId = (existing.length + 1).toString().padStart(6, '0');
140
+
141
+ // Get parent (last event)
142
+ const parent = existing.length > 0
143
+ ? existing.sort().pop()?.replace('.json', '')
144
+ : undefined;
145
+
146
+ const event: TimelineEvent = {
147
+ id: nextId,
148
+ timestamp: new Date().toISOString(),
149
+ task: task.trim(),
150
+ summary: summary.trim(),
151
+ reason: reason?.trim() || undefined,
152
+ files: files ? files.split(',').map((f: string) => f.trim()).filter(Boolean) : [],
153
+ tags: tags ? tags.split(',').map((t: string) => t.trim().toLowerCase()).filter(Boolean) : [],
154
+ parent: parent || undefined,
155
+ };
156
+
157
+ writeFileSync(
158
+ join(eventsDir, `${nextId}.json`),
159
+ JSON.stringify(event, null, 2)
160
+ );
161
+
162
+ // Auto-archive if too many events
163
+ if (existing.length >= MAX_EVENTS) {
164
+ this.archive(cwd);
165
+ }
166
+
167
+ return {
168
+ success: true,
169
+ output: `Event [${nextId}] logged: ${event.task}`,
170
+ };
171
+ }
172
+
173
+ // ── History ───────────────────────────────────────────
174
+
175
+ private history(cwd: string, limit: number): ToolResult {
176
+ const events = this.loadEvents(cwd);
177
+ if (events.length === 0) {
178
+ return { success: true, output: 'No events yet. Use nwt log to start tracking.' };
179
+ }
180
+
181
+ const recent = events.slice(-limit);
182
+ const lines = recent.map(e => {
183
+ const time = e.timestamp.split('T')[0];
184
+ const files = e.files.length > 0 ? ` [${e.files.join(', ')}]` : '';
185
+ const tags = e.tags.length > 0 ? ` {${e.tags.join(', ')}}` : '';
186
+ return `[${e.id}] ${time} ${e.task}${files}${tags}`;
187
+ });
188
+
189
+ return { success: true, output: lines.join('\n') };
190
+ }
191
+
192
+ // ── Search ────────────────────────────────────────────
193
+
194
+ private search(cwd: string, query: string, limit: number): ToolResult {
195
+ if (!query) return { success: false, error: 'query is required' };
196
+
197
+ const events = this.loadEvents(cwd);
198
+ const q = query.toLowerCase();
199
+ const matches = events.filter(e =>
200
+ e.task.toLowerCase().includes(q) ||
201
+ e.summary.toLowerCase().includes(q) ||
202
+ (e.reason || '').toLowerCase().includes(q) ||
203
+ e.files.some(f => f.toLowerCase().includes(q)) ||
204
+ e.tags.some(t => t.includes(q))
205
+ ).slice(-limit);
206
+
207
+ if (matches.length === 0) {
208
+ return { success: true, output: `No events matching "${query}"` };
209
+ }
210
+
211
+ const lines = matches.map(e => {
212
+ const time = e.timestamp.split('T')[0];
213
+ return `[${e.id}] ${time} ${e.task}\n ${e.summary}`;
214
+ });
215
+
216
+ return { success: true, output: lines.join('\n\n') };
217
+ }
218
+
219
+ // ── Story ─────────────────────────────────────────────
220
+
221
+ private story(cwd: string): ToolResult {
222
+ const events = this.loadEvents(cwd);
223
+ if (events.length === 0) {
224
+ return { success: true, output: 'No events yet.' };
225
+ }
226
+
227
+ // Group by tags and find milestones
228
+ const milestones = events.filter(e => e.tags.includes('milestone'));
229
+ const decisions = events.filter(e => e.tags.includes('decision'));
230
+ const totalFiles = new Set(events.flatMap(e => e.files)).size;
231
+
232
+ const lines = [
233
+ `Project Story: ${events.length} events, ${totalFiles} files touched`,
234
+ '',
235
+ ];
236
+
237
+ if (milestones.length > 0) {
238
+ lines.push('Milestones:');
239
+ for (const m of milestones.slice(-5)) {
240
+ lines.push(` [${m.id}] ${m.task} - ${m.summary}`);
241
+ }
242
+ lines.push('');
243
+ }
244
+
245
+ if (decisions.length > 0) {
246
+ lines.push('Key Decisions:');
247
+ for (const d of decisions.slice(-5)) {
248
+ lines.push(` [${d.id}] ${d.task}${d.reason ? ` - ${d.reason}` : ''}`);
249
+ }
250
+ lines.push('');
251
+ }
252
+
253
+ lines.push('Recent Activity:');
254
+ for (const e of events.slice(-5)) {
255
+ const time = e.timestamp.split('T')[0];
256
+ lines.push(` ${time} ${e.task}`);
257
+ }
258
+
259
+ return { success: true, output: lines.join('\n') };
260
+ }
261
+
262
+ // ── Explain File ──────────────────────────────────────
263
+
264
+ private explain(cwd: string, filePath: string): ToolResult {
265
+ if (!filePath) return { success: false, error: 'file path is required' };
266
+
267
+ const events = this.loadEvents(cwd);
268
+ const matches = events.filter(e =>
269
+ e.files.some(f => f === filePath || f.endsWith(filePath))
270
+ );
271
+
272
+ if (matches.length === 0) {
273
+ return { success: true, output: `No history for "${filePath}"` };
274
+ }
275
+
276
+ const lines = matches.map(e => {
277
+ const time = e.timestamp.split('T')[0];
278
+ return `[${e.id}] ${time} ${e.task}\n ${e.summary}${e.reason ? `\n Reason: ${e.reason}` : ''}`;
279
+ });
280
+
281
+ return { success: true, output: `History of ${filePath}:\n\n${lines.join('\n\n')}` };
282
+ }
283
+
284
+ // ── Archive ───────────────────────────────────────────
285
+
286
+ private archive(cwd: string): ToolResult {
287
+ const nwtDir = join(cwd, NWT_DIR);
288
+ const eventsDir = join(nwtDir, EVENTS_DIR);
289
+ const archiveDir = join(nwtDir, ARCHIVE_DIR);
290
+
291
+ if (!existsSync(eventsDir)) {
292
+ return { success: true, output: 'No events to archive.' };
293
+ }
294
+
295
+ const cutoff = new Date();
296
+ cutoff.setDate(cutoff.getDate() - ARCHIVE_AFTER_DAYS);
297
+
298
+ const files = readdirSync(eventsDir).filter(f => f.endsWith('.json')).sort();
299
+ const toArchive: string[] = [];
300
+ const toKeep: string[] = [];
301
+
302
+ for (const file of files) {
303
+ try {
304
+ const event = JSON.parse(readFileSync(join(eventsDir, file), 'utf-8'));
305
+ const eventDate = new Date(event.timestamp);
306
+ if (eventDate < cutoff) {
307
+ toArchive.push(file);
308
+ } else {
309
+ toKeep.push(file);
310
+ }
311
+ } catch {
312
+ toKeep.push(file);
313
+ }
314
+ }
315
+
316
+ if (toArchive.length === 0) {
317
+ return { success: true, output: 'No events old enough to archive.' };
318
+ }
319
+
320
+ // Create archive file
321
+ const archiveName = `archive-${new Date().toISOString().split('T')[0]}.json`;
322
+ const archiveEvents = toArchive.map(f => {
323
+ return JSON.parse(readFileSync(join(eventsDir, f), 'utf-8'));
324
+ });
325
+
326
+ writeFileSync(
327
+ join(archiveDir, archiveName),
328
+ JSON.stringify(archiveEvents, null, 2)
329
+ );
330
+
331
+ // Remove archived events from events dir
332
+ for (const file of toArchive) {
333
+ const src = join(eventsDir, file);
334
+ try { renameSync(src, join(archiveDir, file)); } catch {}
335
+ }
336
+
337
+ return {
338
+ success: true,
339
+ output: `Archived ${toArchive.length} events to archives/${archiveName}. ${toKeep.length} events remain.`,
340
+ };
341
+ }
342
+
343
+ // ── Helpers ───────────────────────────────────────────
344
+
345
+ private loadEvents(cwd: string): TimelineEvent[] {
346
+ const eventsDir = join(cwd, NWT_DIR, EVENTS_DIR);
347
+ if (!existsSync(eventsDir)) return [];
348
+
349
+ const files = readdirSync(eventsDir).filter(f => f.endsWith('.json')).sort();
350
+ const events: TimelineEvent[] = [];
351
+
352
+ for (const file of files) {
353
+ try {
354
+ events.push(JSON.parse(readFileSync(join(eventsDir, file), 'utf-8')));
355
+ } catch {
356
+ // Skip corrupted files
357
+ }
358
+ }
359
+
360
+ return events;
361
+ }
362
+ }
package/src/tui/app.tsx CHANGED
@@ -1,10 +1,11 @@
1
1
  /** @jsxImportSource react */
2
- import React, { useState, useCallback, useEffect } from 'react';
3
- import { Box, Text, useApp, useStdout } from 'ink';
2
+ import React, { useState, useCallback } from 'react';
3
+ import { Box, Text, useStdout } from 'ink';
4
4
  import { Header } from './components/Header.js';
5
5
  import { ChatList } from './components/ChatList.js';
6
6
  import { Thinking } from './components/Thinking.js';
7
7
  import { UserInput } from './components/UserInput.js';
8
+ import { StatusBar } from './components/StatusBar.js';
8
9
  import { useChat } from './hooks/useChat.js';
9
10
  import { useCommands } from './hooks/useCommands.js';
10
11
  import type { App } from '../app/index.js';
@@ -19,10 +20,10 @@ export function TuiApp({ app }: Props) {
19
20
  const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
20
21
  const { handleCommand } = useCommands(app);
21
22
  const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
22
- const { exit } = useApp();
23
+ const { stdout } = useStdout();
24
+ const terminalWidth = stdout?.columns || 80;
23
25
 
24
26
  const onSubmit = useCallback(async (input: string) => {
25
- // Check for built-in commands first
26
27
  const result = handleCommand(input);
27
28
  if (result.handled) {
28
29
  if (result.output) {
@@ -32,29 +33,29 @@ export function TuiApp({ app }: Props) {
32
33
  { role: 'assistant', content: result.output! },
33
34
  ]);
34
35
  }
35
- // Handle clear
36
36
  if (input.toLowerCase().trim() === 'clear') {
37
37
  setSystemMessages([]);
38
38
  }
39
39
  return;
40
40
  }
41
-
42
- // Send to AI
43
41
  await sendMessage(input);
44
42
  }, [handleCommand, sendMessage]);
45
43
 
46
44
  const allMessages = [...systemMessages, ...messages];
45
+ const activeSkills = app.skills.listActive().map(s => s.id);
47
46
 
48
47
  return (
49
- <Box flexDirection="column" padding={1}>
48
+ <Box flexDirection="column" paddingX={1}>
50
49
  <Header provider={config.provider} model={config.model} />
51
50
  <ChatList
52
51
  messages={allMessages}
53
52
  streaming={streaming}
54
53
  streamingToolCalls={streamingToolCalls}
54
+ width={terminalWidth - 4}
55
55
  />
56
56
  {isThinking && <Thinking />}
57
57
  <UserInput onSubmit={onSubmit} disabled={isThinking} />
58
+ <StatusBar messageCount={allMessages.length} skills={activeSkills} />
58
59
  </Box>
59
60
  );
60
61
  }
@@ -1,19 +1,21 @@
1
1
  /** @jsxImportSource react */
2
- import React from 'react';
2
+ import React, { memo } from 'react';
3
3
  import { Box } from 'ink';
4
4
  import { ChatMessage, type MessageData } from './ChatMessage.js';
5
+ import type { ToolCallData } from './ToolCall.js';
5
6
 
6
7
  interface Props {
7
8
  messages: MessageData[];
8
9
  streaming?: string;
9
- streamingToolCalls?: Array<{ name: string; args: string }>;
10
+ streamingToolCalls?: ToolCallData[];
11
+ width?: number;
10
12
  }
11
13
 
12
- export function ChatList({ messages, streaming, streamingToolCalls }: Props) {
14
+ export const ChatList = memo(function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
13
15
  return (
14
16
  <Box flexDirection="column" flexGrow={1}>
15
17
  {messages.map((msg, i) => (
16
- <ChatMessage key={i} message={msg} />
18
+ <ChatMessage key={i} message={msg} width={width} />
17
19
  ))}
18
20
  {(streaming || streamingToolCalls) && (
19
21
  <ChatMessage
@@ -22,8 +24,9 @@ export function ChatList({ messages, streaming, streamingToolCalls }: Props) {
22
24
  content: streaming || '',
23
25
  toolCalls: streamingToolCalls,
24
26
  }}
27
+ width={width}
25
28
  />
26
29
  )}
27
30
  </Box>
28
31
  );
29
- }
32
+ });
@@ -12,40 +12,43 @@ interface MessageData {
12
12
 
13
13
  interface Props {
14
14
  message: MessageData;
15
+ width?: number;
15
16
  }
16
17
 
17
- function UserMessage({ content }: { content: string }) {
18
+ function UserMessage({ content, width }: { content: string; width?: number }) {
18
19
  return (
19
- <Box flexDirection="column" marginBottom={1}>
20
- <Text bold>You</Text>
21
- <Box paddingLeft={2}>
20
+ <Box borderStyle="round" borderLeft borderColor="#06B6D4" paddingLeft={1} marginBottom={1}>
21
+ <Box flexDirection="column" width={width ? width - 4 : undefined}>
22
+ <Text bold color="#06B6D4">You</Text>
22
23
  <Text>{content}</Text>
23
24
  </Box>
24
25
  </Box>
25
26
  );
26
27
  }
27
28
 
28
- function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
29
+ function AssistantMessage({ content, toolCalls, width }: { content: string; toolCalls?: ToolCallData[]; width?: number }) {
29
30
  return (
30
31
  <Box flexDirection="column" marginBottom={1}>
31
- <Text bold color="cyan">AI</Text>
32
32
  {toolCalls && toolCalls.map((tc, i) => (
33
- <ToolCall key={i} tool={tc} />
33
+ <ToolCall key={i} tool={tc} width={width} />
34
34
  ))}
35
35
  {content && (
36
- <Box paddingLeft={2} flexDirection="column">
37
- <Markdown content={content} />
36
+ <Box borderStyle="round" borderLeft borderColor="#22D3EE" paddingLeft={1}>
37
+ <Box flexDirection="column" width={width ? width - 4 : undefined}>
38
+ <Text bold color="#22D3EE">AI</Text>
39
+ <Markdown content={content} />
40
+ </Box>
38
41
  </Box>
39
42
  )}
40
43
  </Box>
41
44
  );
42
45
  }
43
46
 
44
- export function ChatMessage({ message }: Props) {
47
+ export const ChatMessage = React.memo(function ChatMessage({ message, width }: Props) {
45
48
  if (message.role === 'user') {
46
- return <UserMessage content={message.content} />;
49
+ return <UserMessage content={message.content} width={width} />;
47
50
  }
48
- return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
49
- }
51
+ return <AssistantMessage content={message.content} toolCalls={message.toolCalls} width={width} />;
52
+ });
50
53
 
51
54
  export type { MessageData };
@@ -7,16 +7,22 @@ interface Props {
7
7
  model: string;
8
8
  }
9
9
 
10
- export function Header({ provider, model }: Props) {
10
+ export const Header = React.memo(function Header({ provider, model }: Props) {
11
11
  return (
12
- <Box flexDirection="column" marginBottom={1}>
13
- <Box>
14
- <Text bold color="cyan"> ⚡ Thatgfsj Code </Text>
15
- <Text dimColor> v0.6.0</Text>
16
- <Text dimColor> </Text>
17
- <Text dimColor>{provider} / {model}</Text>
12
+ <Box flexDirection="column" marginBottom={0}>
13
+ <Box justifyContent="space-between" width="100%">
14
+ <Box>
15
+ <Text color="#06B6D4" bold> </Text>
16
+ <Text color="#22D3EE" bold>THATGFSJ CODE</Text>
17
+ <Text dimColor> v0.7.1</Text>
18
+ </Box>
19
+ <Box>
20
+ <Text color="#06B6D4" bold> {provider} </Text>
21
+ <Text dimColor>/</Text>
22
+ <Text color="#22D3EE"> {model} </Text>
23
+ </Box>
18
24
  </Box>
19
- <Text dimColor>{'─'.repeat(60)}</Text>
25
+ <Text color="#374151">{'─'.repeat(80)}</Text>
20
26
  </Box>
21
27
  );
22
- }
28
+ });
@@ -0,0 +1,30 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+
5
+ interface Props {
6
+ messageCount: number;
7
+ skills: string[];
8
+ }
9
+
10
+ export const StatusBar = React.memo(function StatusBar({ messageCount, skills }: Props) {
11
+ const activeSkills = skills.slice(0, 3).join(', ');
12
+ const moreSkills = skills.length > 3 ? ` +${skills.length - 3}` : '';
13
+
14
+ return (
15
+ <Box flexDirection="column" marginTop={0}>
16
+ <Text color="#374151">{'─'.repeat(80)}</Text>
17
+ <Box justifyContent="space-between" width="100%">
18
+ <Box>
19
+ <Text backgroundColor="#06B6D4" color="#0F172A" bold> ⚡ THATGFSJ CODE </Text>
20
+ <Text dimColor> │ {messageCount} messages</Text>
21
+ </Box>
22
+ <Box>
23
+ {skills.length > 0 && (
24
+ <Text dimColor> skills: {activeSkills}{moreSkills} </Text>
25
+ )}
26
+ </Box>
27
+ </Box>
28
+ </Box>
29
+ );
30
+ });
@@ -9,11 +9,11 @@ interface Props {
9
9
 
10
10
  export function Thinking({ message = 'Thinking' }: Props) {
11
11
  return (
12
- <Box>
13
- <Text color="cyan">
12
+ <Box paddingLeft={1}>
13
+ <Text color="#06B6D4">
14
14
  <Spinner type="dots" />
15
15
  </Text>
16
- <Text dimColor> {message}</Text>
16
+ <Text color="#06B6D4"> {message}</Text>
17
17
  </Box>
18
18
  );
19
19
  }