thatgfsj-code 0.5.0 → 0.7.0

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 (185) hide show
  1. package/README.md +7 -12
  2. package/dist/app/index.d.ts +2 -0
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +11 -6
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cmd/index.d.ts +1 -2
  7. package/dist/cmd/index.d.ts.map +1 -1
  8. package/dist/cmd/index.js +26 -23
  9. package/dist/cmd/index.js.map +1 -1
  10. package/dist/config/index.d.ts.map +1 -1
  11. package/dist/config/index.js +1 -0
  12. package/dist/config/index.js.map +1 -1
  13. package/dist/config/types.d.ts +1 -0
  14. package/dist/config/types.d.ts.map +1 -1
  15. package/dist/prompts/index.d.ts +2 -0
  16. package/dist/prompts/index.d.ts.map +1 -1
  17. package/dist/prompts/index.js +7 -0
  18. package/dist/prompts/index.js.map +1 -1
  19. package/dist/skills/brainstorming.d.ts +3 -0
  20. package/dist/skills/brainstorming.d.ts.map +1 -0
  21. package/dist/skills/brainstorming.js +42 -0
  22. package/dist/skills/brainstorming.js.map +1 -0
  23. package/dist/skills/code-review.d.ts +3 -0
  24. package/dist/skills/code-review.d.ts.map +1 -0
  25. package/dist/skills/code-review.js +44 -0
  26. package/dist/skills/code-review.js.map +1 -0
  27. package/dist/skills/executing-plans.d.ts +3 -0
  28. package/dist/skills/executing-plans.d.ts.map +1 -0
  29. package/dist/skills/executing-plans.js +26 -0
  30. package/dist/skills/executing-plans.js.map +1 -0
  31. package/dist/skills/frontend-design.d.ts +3 -0
  32. package/dist/skills/frontend-design.d.ts.map +1 -0
  33. package/dist/skills/frontend-design.js +34 -0
  34. package/dist/skills/frontend-design.js.map +1 -0
  35. package/dist/skills/git-workflow.d.ts +3 -0
  36. package/dist/skills/git-workflow.d.ts.map +1 -0
  37. package/dist/skills/git-workflow.js +35 -0
  38. package/dist/skills/git-workflow.js.map +1 -0
  39. package/dist/skills/improve-architecture.d.ts +3 -0
  40. package/dist/skills/improve-architecture.d.ts.map +1 -0
  41. package/dist/skills/improve-architecture.js +37 -0
  42. package/dist/skills/improve-architecture.js.map +1 -0
  43. package/dist/skills/index.d.ts +50 -0
  44. package/dist/skills/index.d.ts.map +1 -0
  45. package/dist/skills/index.js +98 -0
  46. package/dist/skills/index.js.map +1 -0
  47. package/dist/skills/neuroweave.d.ts +3 -0
  48. package/dist/skills/neuroweave.d.ts.map +1 -0
  49. package/dist/skills/neuroweave.js +46 -0
  50. package/dist/skills/neuroweave.js.map +1 -0
  51. package/dist/skills/playwright.d.ts +3 -0
  52. package/dist/skills/playwright.d.ts.map +1 -0
  53. package/dist/skills/playwright.js +71 -0
  54. package/dist/skills/playwright.js.map +1 -0
  55. package/dist/skills/prototype.d.ts +3 -0
  56. package/dist/skills/prototype.d.ts.map +1 -0
  57. package/dist/skills/prototype.js +29 -0
  58. package/dist/skills/prototype.js.map +1 -0
  59. package/dist/skills/subagent.d.ts +3 -0
  60. package/dist/skills/subagent.d.ts.map +1 -0
  61. package/dist/skills/subagent.js +27 -0
  62. package/dist/skills/subagent.js.map +1 -0
  63. package/dist/skills/supabase.d.ts +3 -0
  64. package/dist/skills/supabase.d.ts.map +1 -0
  65. package/dist/skills/supabase.js +43 -0
  66. package/dist/skills/supabase.js.map +1 -0
  67. package/dist/skills/systematic-debugging.d.ts +3 -0
  68. package/dist/skills/systematic-debugging.d.ts.map +1 -0
  69. package/dist/skills/systematic-debugging.js +43 -0
  70. package/dist/skills/systematic-debugging.js.map +1 -0
  71. package/dist/skills/tdd.d.ts +3 -0
  72. package/dist/skills/tdd.d.ts.map +1 -0
  73. package/dist/skills/tdd.js +38 -0
  74. package/dist/skills/tdd.js.map +1 -0
  75. package/dist/skills/triage.d.ts +3 -0
  76. package/dist/skills/triage.d.ts.map +1 -0
  77. package/dist/skills/triage.js +34 -0
  78. package/dist/skills/triage.js.map +1 -0
  79. package/dist/skills/verification.d.ts +3 -0
  80. package/dist/skills/verification.d.ts.map +1 -0
  81. package/dist/skills/verification.js +30 -0
  82. package/dist/skills/verification.js.map +1 -0
  83. package/dist/skills/writing-plans.d.ts +3 -0
  84. package/dist/skills/writing-plans.d.ts.map +1 -0
  85. package/dist/skills/writing-plans.js +41 -0
  86. package/dist/skills/writing-plans.js.map +1 -0
  87. package/dist/tools/index.d.ts +1 -0
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +3 -0
  90. package/dist/tools/index.js.map +1 -1
  91. package/dist/tools/nwt.d.ts +70 -0
  92. package/dist/tools/nwt.d.ts.map +1 -0
  93. package/dist/tools/nwt.js +289 -0
  94. package/dist/tools/nwt.js.map +1 -0
  95. package/dist/tui/app.d.ts +9 -0
  96. package/dist/tui/app.d.ts.map +1 -0
  97. package/dist/tui/app.js +40 -0
  98. package/dist/tui/app.js.map +1 -0
  99. package/dist/tui/components/ChatList.d.ts +13 -0
  100. package/dist/tui/components/ChatList.d.ts.map +1 -0
  101. package/dist/tui/components/ChatList.js +11 -0
  102. package/dist/tui/components/ChatList.js.map +1 -0
  103. package/dist/tui/components/ChatMessage.d.ts +15 -0
  104. package/dist/tui/components/ChatMessage.d.ts.map +1 -0
  105. package/dist/tui/components/ChatMessage.js +17 -0
  106. package/dist/tui/components/ChatMessage.js.map +1 -0
  107. package/dist/tui/components/Header.d.ts +9 -0
  108. package/dist/tui/components/Header.d.ts.map +1 -0
  109. package/dist/tui/components/Header.js +6 -0
  110. package/dist/tui/components/Header.js.map +1 -0
  111. package/dist/tui/components/Markdown.d.ts +8 -0
  112. package/dist/tui/components/Markdown.d.ts.map +1 -0
  113. package/dist/tui/components/Markdown.js +31 -0
  114. package/dist/tui/components/Markdown.js.map +1 -0
  115. package/dist/tui/components/StatusBar.d.ts +9 -0
  116. package/dist/tui/components/StatusBar.d.ts.map +1 -0
  117. package/dist/tui/components/StatusBar.js +8 -0
  118. package/dist/tui/components/StatusBar.js.map +1 -0
  119. package/dist/tui/components/Thinking.d.ts +8 -0
  120. package/dist/tui/components/Thinking.d.ts.map +1 -0
  121. package/dist/tui/components/Thinking.js +7 -0
  122. package/dist/tui/components/Thinking.js.map +1 -0
  123. package/dist/tui/components/ToolCall.d.ts +15 -0
  124. package/dist/tui/components/ToolCall.d.ts.map +1 -0
  125. package/dist/tui/components/ToolCall.js +42 -0
  126. package/dist/tui/components/ToolCall.js.map +1 -0
  127. package/dist/tui/components/UserInput.d.ts +9 -0
  128. package/dist/tui/components/UserInput.d.ts.map +1 -0
  129. package/dist/tui/components/UserInput.js +76 -0
  130. package/dist/tui/components/UserInput.js.map +1 -0
  131. package/dist/tui/hooks/useChat.d.ts +11 -0
  132. package/dist/tui/hooks/useChat.d.ts.map +1 -0
  133. package/dist/tui/hooks/useChat.js +138 -0
  134. package/dist/tui/hooks/useChat.js.map +1 -0
  135. package/dist/tui/hooks/useCommands.d.ts +10 -0
  136. package/dist/tui/hooks/useCommands.d.ts.map +1 -0
  137. package/dist/tui/hooks/useCommands.js +78 -0
  138. package/dist/tui/hooks/useCommands.js.map +1 -0
  139. package/dist/tui/index.d.ts +2 -4
  140. package/dist/tui/index.d.ts.map +1 -1
  141. package/dist/tui/index.js +2 -4
  142. package/dist/tui/index.js.map +1 -1
  143. package/dist/tui/welcome.d.ts.map +1 -1
  144. package/dist/tui/welcome.js +22 -4
  145. package/dist/tui/welcome.js.map +1 -1
  146. package/install.ps1 +171 -192
  147. package/package.json +9 -1
  148. package/src/app/index.ts +13 -7
  149. package/src/cmd/{index.ts → index.tsx} +28 -27
  150. package/src/config/index.ts +1 -0
  151. package/src/config/types.ts +1 -0
  152. package/src/prompts/index.ts +8 -0
  153. package/src/skills/brainstorming.ts +43 -0
  154. package/src/skills/code-review.ts +45 -0
  155. package/src/skills/executing-plans.ts +27 -0
  156. package/src/skills/frontend-design.ts +35 -0
  157. package/src/skills/git-workflow.ts +36 -0
  158. package/src/skills/improve-architecture.ts +38 -0
  159. package/src/skills/index.ts +136 -0
  160. package/src/skills/neuroweave.ts +47 -0
  161. package/src/skills/playwright.ts +72 -0
  162. package/src/skills/prototype.ts +30 -0
  163. package/src/skills/subagent.ts +28 -0
  164. package/src/skills/supabase.ts +44 -0
  165. package/src/skills/systematic-debugging.ts +44 -0
  166. package/src/skills/tdd.ts +39 -0
  167. package/src/skills/triage.ts +35 -0
  168. package/src/skills/verification.ts +31 -0
  169. package/src/skills/writing-plans.ts +42 -0
  170. package/src/tools/index.ts +3 -0
  171. package/src/tools/nwt.ts +362 -0
  172. package/src/tui/app.tsx +61 -0
  173. package/src/tui/components/ChatList.tsx +32 -0
  174. package/src/tui/components/ChatMessage.tsx +54 -0
  175. package/src/tui/components/Header.tsx +28 -0
  176. package/src/tui/components/Markdown.tsx +35 -0
  177. package/src/tui/components/StatusBar.tsx +30 -0
  178. package/src/tui/components/Thinking.tsx +19 -0
  179. package/src/tui/components/ToolCall.tsx +74 -0
  180. package/src/tui/components/UserInput.tsx +83 -0
  181. package/src/tui/hooks/useChat.ts +154 -0
  182. package/src/tui/hooks/useCommands.ts +91 -0
  183. package/src/tui/index.ts +2 -4
  184. package/src/tui/welcome.ts +24 -5
  185. package/tsconfig.json +5 -3
@@ -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
+ }
@@ -0,0 +1,61 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useState, useCallback } from 'react';
3
+ import { Box, Text, useStdout } from 'ink';
4
+ import { Header } from './components/Header.js';
5
+ import { ChatList } from './components/ChatList.js';
6
+ import { Thinking } from './components/Thinking.js';
7
+ import { UserInput } from './components/UserInput.js';
8
+ import { StatusBar } from './components/StatusBar.js';
9
+ import { useChat } from './hooks/useChat.js';
10
+ import { useCommands } from './hooks/useCommands.js';
11
+ import type { App } from '../app/index.js';
12
+ import type { MessageData } from './components/ChatMessage.js';
13
+
14
+ interface Props {
15
+ app: App;
16
+ }
17
+
18
+ export function TuiApp({ app }: Props) {
19
+ const config = app.config.get();
20
+ const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
21
+ const { handleCommand } = useCommands(app);
22
+ const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
23
+ const { stdout } = useStdout();
24
+ const terminalWidth = stdout?.columns || 80;
25
+
26
+ const onSubmit = useCallback(async (input: string) => {
27
+ const result = handleCommand(input);
28
+ if (result.handled) {
29
+ if (result.output) {
30
+ setSystemMessages(prev => [
31
+ ...prev,
32
+ { role: 'user', content: input },
33
+ { role: 'assistant', content: result.output! },
34
+ ]);
35
+ }
36
+ if (input.toLowerCase().trim() === 'clear') {
37
+ setSystemMessages([]);
38
+ }
39
+ return;
40
+ }
41
+ await sendMessage(input);
42
+ }, [handleCommand, sendMessage]);
43
+
44
+ const allMessages = [...systemMessages, ...messages];
45
+ const activeSkills = app.skills.listActive().map(s => s.id);
46
+
47
+ return (
48
+ <Box flexDirection="column" paddingX={1}>
49
+ <Header provider={config.provider} model={config.model} />
50
+ <ChatList
51
+ messages={allMessages}
52
+ streaming={streaming}
53
+ streamingToolCalls={streamingToolCalls}
54
+ width={terminalWidth - 4}
55
+ />
56
+ {isThinking && <Thinking />}
57
+ <UserInput onSubmit={onSubmit} disabled={isThinking} />
58
+ <StatusBar messageCount={allMessages.length} skills={activeSkills} />
59
+ </Box>
60
+ );
61
+ }
@@ -0,0 +1,32 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box } from 'ink';
4
+ import { ChatMessage, type MessageData } from './ChatMessage.js';
5
+ import type { ToolCallData } from './ToolCall.js';
6
+
7
+ interface Props {
8
+ messages: MessageData[];
9
+ streaming?: string;
10
+ streamingToolCalls?: ToolCallData[];
11
+ width?: number;
12
+ }
13
+
14
+ export function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
15
+ return (
16
+ <Box flexDirection="column" flexGrow={1}>
17
+ {messages.map((msg, i) => (
18
+ <ChatMessage key={i} message={msg} width={width} />
19
+ ))}
20
+ {(streaming || streamingToolCalls) && (
21
+ <ChatMessage
22
+ message={{
23
+ role: 'assistant',
24
+ content: streaming || '',
25
+ toolCalls: streamingToolCalls,
26
+ }}
27
+ width={width}
28
+ />
29
+ )}
30
+ </Box>
31
+ );
32
+ }
@@ -0,0 +1,54 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { Markdown } from './Markdown.js';
5
+ import { ToolCall, type ToolCallData } from './ToolCall.js';
6
+
7
+ interface MessageData {
8
+ role: 'user' | 'assistant' | 'tool';
9
+ content: string;
10
+ toolCalls?: ToolCallData[];
11
+ }
12
+
13
+ interface Props {
14
+ message: MessageData;
15
+ width?: number;
16
+ }
17
+
18
+ function UserMessage({ content, width }: { content: string; width?: number }) {
19
+ return (
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>
23
+ <Text>{content}</Text>
24
+ </Box>
25
+ </Box>
26
+ );
27
+ }
28
+
29
+ function AssistantMessage({ content, toolCalls, width }: { content: string; toolCalls?: ToolCallData[]; width?: number }) {
30
+ return (
31
+ <Box flexDirection="column" marginBottom={1}>
32
+ {toolCalls && toolCalls.map((tc, i) => (
33
+ <ToolCall key={i} tool={tc} width={width} />
34
+ ))}
35
+ {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>
41
+ </Box>
42
+ )}
43
+ </Box>
44
+ );
45
+ }
46
+
47
+ export function ChatMessage({ message, width }: Props) {
48
+ if (message.role === 'user') {
49
+ return <UserMessage content={message.content} width={width} />;
50
+ }
51
+ return <AssistantMessage content={message.content} toolCalls={message.toolCalls} width={width} />;
52
+ }
53
+
54
+ export type { MessageData };
@@ -0,0 +1,28 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+
5
+ interface Props {
6
+ provider: string;
7
+ model: string;
8
+ }
9
+
10
+ export function Header({ provider, model }: Props) {
11
+ return (
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.0</Text>
18
+ </Box>
19
+ <Box>
20
+ <Text color="#06B6D4" bold> {provider} </Text>
21
+ <Text dimColor>/</Text>
22
+ <Text color="#22D3EE"> {model} </Text>
23
+ </Box>
24
+ </Box>
25
+ <Text color="#374151">{'─'.repeat(80)}</Text>
26
+ </Box>
27
+ );
28
+ }
@@ -0,0 +1,35 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useMemo } from 'react';
3
+ import { Text } from 'ink';
4
+ import { marked } from 'marked';
5
+ import TerminalRenderer from 'marked-terminal';
6
+
7
+ // Configure marked to use terminal renderer
8
+ marked.setOptions({
9
+ // @ts-ignore - marked-terminal types are incomplete
10
+ renderer: new TerminalRenderer({
11
+ showSectionPrefix: false,
12
+ tab: 2,
13
+ }),
14
+ });
15
+
16
+ interface Props {
17
+ content: string;
18
+ }
19
+
20
+ export function Markdown({ content }: Props) {
21
+ const rendered = useMemo(() => {
22
+ try {
23
+ const result = marked.parse(content);
24
+ // marked.parse returns string | Promise<string> depending on async option
25
+ if (typeof result === 'string') {
26
+ return result.trimEnd();
27
+ }
28
+ return content;
29
+ } catch {
30
+ return content;
31
+ }
32
+ }, [content]);
33
+
34
+ return <Text>{rendered}</Text>;
35
+ }
@@ -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 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
+ }
@@ -0,0 +1,19 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+
6
+ interface Props {
7
+ message?: string;
8
+ }
9
+
10
+ export function Thinking({ message = 'Thinking' }: Props) {
11
+ return (
12
+ <Box paddingLeft={1}>
13
+ <Text color="#06B6D4">
14
+ <Spinner type="dots" />
15
+ </Text>
16
+ <Text color="#06B6D4"> {message}</Text>
17
+ </Box>
18
+ );
19
+ }