@intrect/openswarm 0.10.2 → 0.12.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 (86) hide show
  1. package/README.md +18 -61
  2. package/dist/agents/pairPipeline.d.ts.map +1 -1
  3. package/dist/agents/pairPipeline.js +4 -3
  4. package/dist/agents/pairPipeline.js.map +1 -1
  5. package/dist/cli/fixCommand.d.ts +91 -0
  6. package/dist/cli/fixCommand.d.ts.map +1 -0
  7. package/dist/cli/fixCommand.js +214 -0
  8. package/dist/cli/fixCommand.js.map +1 -0
  9. package/dist/cli/reviewAudit.d.ts +72 -1
  10. package/dist/cli/reviewAudit.d.ts.map +1 -1
  11. package/dist/cli/reviewAudit.js +96 -4
  12. package/dist/cli/reviewAudit.js.map +1 -1
  13. package/dist/cli/reviewMaxCommand.d.ts +2 -0
  14. package/dist/cli/reviewMaxCommand.d.ts.map +1 -1
  15. package/dist/cli/reviewMaxCommand.js +38 -9
  16. package/dist/cli/reviewMaxCommand.js.map +1 -1
  17. package/dist/cli/reviewProgress.d.ts +8 -5
  18. package/dist/cli/reviewProgress.d.ts.map +1 -1
  19. package/dist/cli/reviewProgress.js +19 -14
  20. package/dist/cli/reviewProgress.js.map +1 -1
  21. package/dist/cli.js +29 -0
  22. package/dist/cli.js.map +1 -1
  23. package/dist/orchestration/taskScheduler.d.ts.map +1 -1
  24. package/dist/orchestration/taskScheduler.js +10 -1
  25. package/dist/orchestration/taskScheduler.js.map +1 -1
  26. package/dist/runners/cliRunner.d.ts.map +1 -1
  27. package/dist/runners/cliRunner.js +21 -5
  28. package/dist/runners/cliRunner.js.map +1 -1
  29. package/dist/support/colors.d.ts +20 -0
  30. package/dist/support/colors.d.ts.map +1 -1
  31. package/dist/support/colors.js +29 -0
  32. package/dist/support/colors.js.map +1 -1
  33. package/dist/support/glyphs.d.ts +9 -0
  34. package/dist/support/glyphs.d.ts.map +1 -0
  35. package/dist/support/glyphs.js +27 -0
  36. package/dist/support/glyphs.js.map +1 -0
  37. package/dist/tui/components/AuditBoard.d.ts.map +1 -1
  38. package/dist/tui/components/AuditBoard.js +3 -8
  39. package/dist/tui/components/AuditBoard.js.map +1 -1
  40. package/dist/tui/components/StageTimeline.d.ts.map +1 -1
  41. package/dist/tui/components/StageTimeline.js +5 -4
  42. package/dist/tui/components/StageTimeline.js.map +1 -1
  43. package/dist/tui/components/Status.d.ts +14 -0
  44. package/dist/tui/components/Status.d.ts.map +1 -0
  45. package/dist/tui/components/Status.js +33 -0
  46. package/dist/tui/components/Status.js.map +1 -0
  47. package/dist/tui/components/SubagentTree.d.ts.map +1 -1
  48. package/dist/tui/components/SubagentTree.js +4 -4
  49. package/dist/tui/components/SubagentTree.js.map +1 -1
  50. package/dist/tui/loadingMessages.d.ts +1 -3
  51. package/dist/tui/loadingMessages.d.ts.map +1 -1
  52. package/dist/tui/loadingMessages.js +3 -5
  53. package/dist/tui/loadingMessages.js.map +1 -1
  54. package/dist/tui/theme.d.ts +17 -3
  55. package/dist/tui/theme.d.ts.map +1 -1
  56. package/dist/tui/theme.js +27 -5
  57. package/dist/tui/theme.js.map +1 -1
  58. package/package.json +1 -1
  59. package/dist/agents/multiLensReview.d.ts +0 -49
  60. package/dist/agents/multiLensReview.d.ts.map +0 -1
  61. package/dist/agents/multiLensReview.js +0 -148
  62. package/dist/agents/multiLensReview.js.map +0 -1
  63. package/dist/automation/backlogGrooming.d.ts +0 -21
  64. package/dist/automation/backlogGrooming.d.ts.map +0 -1
  65. package/dist/automation/backlogGrooming.js +0 -80
  66. package/dist/automation/backlogGrooming.js.map +0 -1
  67. package/dist/automation/localCI.d.ts +0 -24
  68. package/dist/automation/localCI.d.ts.map +0 -1
  69. package/dist/automation/localCI.js +0 -84
  70. package/dist/automation/localCI.js.map +0 -1
  71. package/dist/support/chat.d.ts +0 -3
  72. package/dist/support/chat.d.ts.map +0 -1
  73. package/dist/support/chat.js +0 -313
  74. package/dist/support/chat.js.map +0 -1
  75. package/dist/support/chatTui.d.ts +0 -3
  76. package/dist/support/chatTui.d.ts.map +0 -1
  77. package/dist/support/chatTui.js +0 -1225
  78. package/dist/support/chatTui.js.map +0 -1
  79. package/dist/support/quotaTracker.d.ts +0 -29
  80. package/dist/support/quotaTracker.d.ts.map +0 -1
  81. package/dist/support/quotaTracker.js +0 -89
  82. package/dist/support/quotaTracker.js.map +0 -1
  83. package/dist/tui/components/StatusBar.d.ts +0 -7
  84. package/dist/tui/components/StatusBar.d.ts.map +0 -1
  85. package/dist/tui/components/StatusBar.js +0 -8
  86. package/dist/tui/components/StatusBar.js.map +0 -1
@@ -1,1225 +0,0 @@
1
- #!/usr/bin/env tsx
2
- // OpenSwarm - Rich TUI Chat Interface
3
- // Claude Code style tabbed interface with real-time updates
4
- import blessed from 'blessed';
5
- import { resolve } from 'node:path';
6
- import { writeFile } from 'node:fs/promises';
7
- import { expandPath } from '../core/config.js';
8
- import { getDefaultChatModel, resolveChatModel, shortenChatModel } from './chatBackend.js';
9
- import { runPlanCommand } from './planCommand.js';
10
- import { saveSession, loadSession, generateSessionId, loadDefaultProvider, callChatModel, } from './chatSession.js';
11
- // Render guard: blessed는 동시 render() 호출 시 화면이 검은색으로 깨질 수 있음
12
- let renderScheduled = false;
13
- let screenRef = null;
14
- function safeRender() {
15
- if (renderScheduled || !screenRef)
16
- return;
17
- renderScheduled = true;
18
- process.nextTick(() => {
19
- renderScheduled = false;
20
- try {
21
- screenRef.render();
22
- }
23
- catch {
24
- // render 실패 시 복구 시도
25
- try {
26
- screenRef.alloc();
27
- screenRef.render();
28
- }
29
- catch {
30
- // 무시 — 다음 사이클에서 복구
31
- }
32
- }
33
- });
34
- }
35
- // Session management, provider resolution, and the model-call wrapper now live
36
- // in chatSession.ts (UI-agnostic, INT-1935) and are imported above.
37
- // Warhammer 40k Loading Messages
38
- const LOADING_MESSAGES = [
39
- 'Initializing cogitator arrays',
40
- 'Querying data-vault archives',
41
- 'Accessing servitor protocols',
42
- 'Compiling neural responses',
43
- 'Interfacing with the Noosphere',
44
- 'Scanning data-streams',
45
- 'Calibrating logic engines',
46
- 'Decoding transmission packets',
47
- 'Loading archive databases',
48
- 'Synchronizing machine protocols',
49
- 'Analyzing pattern matrices',
50
- 'Establishing neural link',
51
- 'Processing data-core output',
52
- 'Running diagnostics sequence',
53
- 'Activating response circuits',
54
- ];
55
- const SPINNER_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
56
- // UI Components - Claude Code Style
57
- // Slash commands shown in the typing palette (mirrors handleCommand cases).
58
- const SLASH_COMMANDS = [
59
- { name: '/goal', args: '<goal>', desc: 'Set a goal & pursue it autonomously' },
60
- { name: '/plan', args: '<goal>', desc: 'Decompose a goal & dispatch to the loop' },
61
- { name: '/model', args: '[name]', desc: 'Switch model' },
62
- { name: '/provider', args: '[name]', desc: 'Switch provider' },
63
- { name: '/clear', args: '', desc: 'Clear the conversation' },
64
- { name: '/save', args: '', desc: 'Save the session' },
65
- { name: '/export', args: '[path]', desc: 'Export the conversation to a .txt file' },
66
- { name: '/help', args: '', desc: 'Show all commands' },
67
- ];
68
- /** Show/hide the slash-command palette based on the current input line. */
69
- function updateCommandPalette(ui) {
70
- const line = (ui.inputBox.getValue().split('\n').pop() ?? '').replace(/^\s+/, '');
71
- const hide = () => {
72
- if (!ui.commandPalette.hidden) {
73
- ui.commandPalette.hide();
74
- ui.screen.render();
75
- }
76
- };
77
- if (!line.startsWith('/'))
78
- return hide();
79
- const q = line.slice(1).toLowerCase();
80
- const matches = SLASH_COMMANDS.filter((c) => c.name.slice(1).startsWith(q));
81
- if (matches.length === 0)
82
- return hide();
83
- ui.commandPalette.setContent(matches
84
- .map((c) => ` {#60a5fa-fg}{bold}${c.name}{/bold}{/}${c.args ? ` {#a0aec0-fg}${c.args}{/}` : ''} {#718096-fg}${c.desc}{/}`)
85
- .join('\n'));
86
- ui.commandPalette.height = matches.length + 2; // content rows + border
87
- ui.commandPalette.show();
88
- ui.commandPalette.setFront();
89
- ui.screen.render();
90
- }
91
- function createUI() {
92
- const screen = blessed.screen({
93
- smartCSR: true,
94
- title: 'OpenSwarm Chat',
95
- fullUnicode: true,
96
- terminal: 'xterm-256color',
97
- forceUnicode: true,
98
- warnings: false, // Suppress warnings that can corrupt display
99
- dockBorders: true, // Better border handling in tmux
100
- fastCSR: true, // Faster rendering for streaming
101
- });
102
- const colors = {
103
- bg: '#1a1a1a',
104
- statusBg: '#2d3748',
105
- statusFg: '#e2e8f0',
106
- tabActiveBg: '#4a5568',
107
- tabActiveFg: '#f7fafc',
108
- tabInactiveBg: '#2d3748',
109
- tabInactiveFg: '#a0aec0',
110
- border: '#4a5568',
111
- borderActive: '#667eea',
112
- inputBorder: '#48bb78',
113
- scrollbar: '#4a5568',
114
- userMessage: '#60a5fa',
115
- assistantMessage: '#34d399',
116
- dimText: '#718096',
117
- };
118
- const statusBar = blessed.box({
119
- top: 0,
120
- left: 0,
121
- width: '100%',
122
- height: 1,
123
- tags: true,
124
- style: {
125
- fg: colors.statusFg,
126
- bg: colors.statusBg,
127
- },
128
- });
129
- const tabBar = blessed.box({
130
- top: 1,
131
- left: 0,
132
- width: '100%',
133
- height: 1,
134
- tags: true,
135
- style: {
136
- fg: colors.tabInactiveFg,
137
- bg: colors.tabInactiveBg,
138
- },
139
- });
140
- // Chat tab content - clean borders (adjusted for taller input box)
141
- const chatLog = blessed.log({
142
- top: 2,
143
- left: 0,
144
- width: '100%',
145
- height: '100%-9', // Increased from -7 to -9 for 5-line input box
146
- scrollable: true,
147
- alwaysScroll: true,
148
- mouse: true,
149
- scrollbar: {
150
- ch: '│',
151
- track: {
152
- bg: '#1a1a1a',
153
- },
154
- style: {
155
- fg: colors.scrollbar,
156
- },
157
- },
158
- tags: true,
159
- border: { type: 'line' },
160
- style: {
161
- fg: '#e2e8f0',
162
- bg: '#1a1a1a',
163
- border: {
164
- fg: colors.border,
165
- },
166
- },
167
- });
168
- const projectsBox = blessed.box({
169
- top: 2,
170
- left: 0,
171
- width: '100%',
172
- height: '100%-9', // Adjusted for taller input box
173
- content: '{center}{#718096-fg}Loading projects...{/}{/center}',
174
- tags: true,
175
- scrollable: true,
176
- mouse: true,
177
- scrollbar: {
178
- ch: '│',
179
- style: { fg: colors.scrollbar },
180
- },
181
- border: { type: 'line' },
182
- style: {
183
- fg: '#e2e8f0',
184
- bg: '#1a1a1a',
185
- border: { fg: colors.border },
186
- },
187
- hidden: true,
188
- });
189
- const tasksBox = blessed.box({
190
- top: 2,
191
- left: 0,
192
- width: '100%',
193
- height: '100%-9', // Adjusted for taller input box
194
- content: '{center}{#718096-fg}Loading tasks...{/}{/center}',
195
- tags: true,
196
- scrollable: true,
197
- mouse: true,
198
- scrollbar: {
199
- ch: '│',
200
- style: { fg: colors.scrollbar },
201
- },
202
- border: { type: 'line' },
203
- style: {
204
- fg: '#e2e8f0',
205
- bg: '#1a1a1a',
206
- border: { fg: colors.border },
207
- },
208
- hidden: true,
209
- });
210
- const stuckBox = blessed.box({
211
- top: 2,
212
- left: 0,
213
- width: '100%',
214
- height: '100%-9',
215
- content: '{center}{#718096-fg}Loading stuck issues...{/}{/center}',
216
- tags: true,
217
- scrollable: true,
218
- alwaysScroll: true,
219
- mouse: true,
220
- keys: true,
221
- vi: true,
222
- scrollbar: {
223
- ch: '█',
224
- style: { fg: colors.scrollbar },
225
- },
226
- style: {
227
- border: { fg: colors.border },
228
- },
229
- hidden: true,
230
- });
231
- const logsBox = blessed.log({
232
- top: 2,
233
- left: 0,
234
- width: '100%',
235
- height: '100%-9', // Adjusted for taller input box
236
- scrollable: true,
237
- alwaysScroll: true,
238
- mouse: true,
239
- scrollbar: {
240
- ch: '│',
241
- style: { fg: colors.scrollbar },
242
- },
243
- tags: true,
244
- border: { type: 'line' },
245
- style: {
246
- fg: '#e2e8f0',
247
- bg: '#1a1a1a',
248
- border: { fg: colors.border },
249
- },
250
- hidden: true,
251
- });
252
- const issuesBox = blessed.box({
253
- top: 2,
254
- left: 0,
255
- width: '100%',
256
- height: '100%-9',
257
- scrollable: true,
258
- alwaysScroll: true,
259
- mouse: true,
260
- scrollbar: {
261
- ch: '│',
262
- style: { fg: colors.scrollbar },
263
- },
264
- tags: true,
265
- border: { type: 'line' },
266
- label: ' {#00ccdd-fg}Issues{/} ',
267
- style: {
268
- border: { fg: colors.border },
269
- },
270
- hidden: true,
271
- });
272
- // Input box - textarea for multiline + Korean support
273
- const inputBox = blessed.textarea({
274
- bottom: 1,
275
- left: 0,
276
- width: '100%',
277
- height: 5, // Increased height for multiline
278
- inputOnFocus: true,
279
- border: { type: 'line' },
280
- label: ' {#718096-fg}Message (Shift+Enter: newline, Enter: send){/} ',
281
- tags: true,
282
- keys: true, // Enable key handling
283
- mouse: true,
284
- scrollable: true,
285
- alwaysScroll: false,
286
- style: {
287
- fg: '#f7fafc',
288
- bg: '#1a1a1a',
289
- border: { fg: colors.border },
290
- focus: {
291
- border: { fg: colors.borderActive },
292
- bg: '#0d1117',
293
- },
294
- },
295
- });
296
- const helpBar = blessed.box({
297
- bottom: 0,
298
- left: 0,
299
- width: '100%',
300
- height: 1,
301
- content: ' {#718096-fg}Tab{/} Switch {#718096-fg}Enter{/} Send {#718096-fg}Shift+Enter{/} Newline {#718096-fg}Esc{/} Exit Input {#718096-fg}i{/} Focus Input {#718096-fg}Ctrl+C{/} Exit {#718096-fg}/help{/} Cmds',
302
- tags: true,
303
- style: {
304
- fg: '#a0aec0',
305
- bg: colors.statusBg,
306
- },
307
- });
308
- // Slash-command palette — floats above the input, shown while typing `/`.
309
- const commandPalette = blessed.box({
310
- bottom: 6, // above the input box (height 5) + helpBar (height 1)
311
- left: 0,
312
- width: '60%',
313
- height: 3,
314
- hidden: true,
315
- tags: true,
316
- border: { type: 'line' },
317
- label: ' {#718096-fg}commands{/} ',
318
- style: { fg: '#e2e8f0', bg: '#2d3748', border: { fg: colors.borderActive } },
319
- });
320
- screen.append(statusBar);
321
- screen.append(tabBar);
322
- screen.append(chatLog);
323
- screen.append(projectsBox);
324
- screen.append(tasksBox);
325
- screen.append(stuckBox);
326
- screen.append(logsBox);
327
- screen.append(issuesBox);
328
- screen.append(inputBox);
329
- screen.append(helpBar);
330
- screen.append(commandPalette);
331
- return {
332
- screen,
333
- statusBar,
334
- tabBar,
335
- chatLog,
336
- projectsBox,
337
- tasksBox,
338
- stuckBox,
339
- logsBox,
340
- issuesBox,
341
- inputBox,
342
- helpBar,
343
- commandPalette,
344
- };
345
- }
346
- // Tab Management
347
- function updateTabBar(ui, currentTab) {
348
- const tabs = [
349
- { key: '1', name: 'Chat', icon: '💬' },
350
- { key: '2', name: 'Projects', icon: '📁' },
351
- { key: '3', name: 'Tasks', icon: '✓' },
352
- { key: '4', name: 'Stuck', icon: '⚠' },
353
- { key: '5', name: 'Issues', icon: '🎫' },
354
- { key: '6', name: 'Logs', icon: '📝' },
355
- ];
356
- const content = tabs.map((tab, idx) => {
357
- if (idx === currentTab) {
358
- // Active tab - highlighted
359
- return `{#4a5568-bg}{#f7fafc-fg}{bold} ${tab.icon} ${tab.name} {/bold}{/}{/}`;
360
- }
361
- // Inactive tab - dimmed
362
- return `{#2d3748-bg}{#a0aec0-fg} ${tab.icon} ${tab.name} {/}{/}`;
363
- }).join(' ');
364
- ui.tabBar.setContent(' ' + content);
365
- }
366
- function switchTab(state, ui, tabIndex) {
367
- state.currentTab = tabIndex;
368
- ui.chatLog.hide();
369
- ui.projectsBox.hide();
370
- ui.tasksBox.hide();
371
- ui.stuckBox.hide();
372
- ui.issuesBox.hide();
373
- ui.logsBox.hide();
374
- switch (tabIndex) {
375
- case 0:
376
- ui.chatLog.show();
377
- break;
378
- case 1:
379
- ui.projectsBox.show();
380
- loadProjectsData(ui.projectsBox);
381
- break;
382
- case 2:
383
- ui.tasksBox.show();
384
- loadTasksData(ui.tasksBox);
385
- break;
386
- case 3:
387
- ui.stuckBox.show();
388
- loadStuckData(ui.stuckBox);
389
- break;
390
- case 4:
391
- ui.issuesBox.show();
392
- loadIssuesData(ui.issuesBox);
393
- break;
394
- case 5:
395
- ui.logsBox.show();
396
- break;
397
- }
398
- updateTabBar(ui, tabIndex);
399
- safeRender();
400
- }
401
- // Data Loaders
402
- async function loadProjectsData(box) {
403
- try {
404
- const response = await fetch('http://127.0.0.1:3847/api/projects');
405
- const projects = await response.json();
406
- if (projects.length === 0) {
407
- box.setContent('\n{center}{#718096-fg}No projects tracked{/}{/center}');
408
- return;
409
- }
410
- const lines = [
411
- '',
412
- ` {#a0aec0-fg}${projects.length} project${projects.length > 1 ? 's' : ''} tracked{/}`,
413
- '',
414
- ];
415
- for (const p of projects) {
416
- const status = p.enabled ? '{#34d399-fg}●{/}' : '{#718096-fg}○{/}';
417
- const running = p.running.length > 0 ? `{#60a5fa-fg}${p.running.length} running{/}` : '';
418
- const queued = p.queued.length > 0 ? `{#f59e0b-fg}${p.queued.length} queued{/}` : '';
419
- const tasks = [running, queued].filter(Boolean).join(' · ');
420
- lines.push(` ${status} {bold}${p.name}{/bold}`);
421
- if (tasks) {
422
- lines.push(` ${tasks}`);
423
- }
424
- lines.push(` {#718096-fg}${p.path}{/}`);
425
- lines.push('');
426
- }
427
- box.setContent(lines.join('\n'));
428
- }
429
- catch (err) {
430
- box.setContent(`\n{center}{#ef4444-fg}Failed to load projects{/}\n{#718096-fg}${err}{/}{/center}`);
431
- }
432
- }
433
- async function loadTasksData(box) {
434
- try {
435
- const response = await fetch('http://127.0.0.1:3847/api/pipeline');
436
- const { stages } = await response.json();
437
- if (stages.length === 0) {
438
- box.setContent('\n{center}{#718096-fg}No pipeline events{/}{/center}');
439
- return;
440
- }
441
- // Build task info map and extract stage events
442
- const taskInfo = new Map();
443
- const allStageEvents = [];
444
- for (const event of stages) {
445
- if (event.type === 'task:started' && event.data.taskId) {
446
- taskInfo.set(event.data.taskId, { title: event.data.title, issueIdentifier: event.data.issueIdentifier });
447
- }
448
- else if (event.type === 'pipeline:stage' && event.data.taskId && event.data.stage) {
449
- allStageEvents.push({
450
- taskId: event.data.taskId,
451
- stage: event.data.stage,
452
- status: event.data.status || '',
453
- model: event.data.model,
454
- inputTokens: event.data.inputTokens,
455
- outputTokens: event.data.outputTokens,
456
- costUsd: event.data.costUsd,
457
- });
458
- }
459
- }
460
- if (allStageEvents.length === 0) {
461
- box.setContent('\n{center}{#718096-fg}No active pipeline stages{/}{/center}');
462
- return;
463
- }
464
- // Render pipeline table
465
- const recentStages = allStageEvents.slice(-15).reverse();
466
- const lines = [
467
- '',
468
- ` {#34d399-fg}{bold}Pipeline Events{/bold} {#718096-fg}(${recentStages.length} recent){/}{/}`,
469
- '',
470
- ` {#718096-fg}${'TASK'.padEnd(12)} ${'STAGE'.padEnd(10)} ${'MODEL'.padEnd(12)} ${'TOKENS'.padEnd(15)} STATUS{/}`,
471
- ` {#444444-fg}${'─'.repeat(70)}{/}`,
472
- ];
473
- for (const ev of recentStages) {
474
- const info = taskInfo.get(ev.taskId);
475
- const task = (info?.issueIdentifier || ev.taskId.slice(0, 8)).padEnd(12).slice(0, 12);
476
- const stage = ev.stage.padEnd(10).slice(0, 10);
477
- const statusMap = {
478
- start: ['◐', '#f59e0b'],
479
- complete: ['●', '#34d399'],
480
- fail: ['✗', '#ef4444'],
481
- };
482
- const [icon, color] = statusMap[ev.status] || ['○', '#718096'];
483
- let model = '';
484
- if (ev.model?.includes('sonnet-4-6'))
485
- model = 'sonnet-4.6';
486
- else if (ev.model?.includes('sonnet-4-5'))
487
- model = 'sonnet-4.5';
488
- else if (ev.model?.includes('haiku-4-5'))
489
- model = 'haiku-4.5';
490
- else if (ev.model?.includes('opus-4-7'))
491
- model = 'opus-4.7';
492
- else if (ev.model?.includes('opus-4'))
493
- model = 'opus-4';
494
- else if (ev.model)
495
- model = ev.model.split('-').pop() || '';
496
- model = model.padEnd(12).slice(0, 12);
497
- let tokens = '';
498
- if (ev.inputTokens || ev.outputTokens) {
499
- const inK = ev.inputTokens ? Math.round(ev.inputTokens / 1000) : 0;
500
- const outK = ev.outputTokens ? Math.round(ev.outputTokens / 1000) : 0;
501
- tokens = `${inK}k/${outK}k`;
502
- if (ev.costUsd != null)
503
- tokens += ` $${ev.costUsd.toFixed(2)}`;
504
- }
505
- tokens = tokens.padEnd(15).slice(0, 15);
506
- lines.push(` {#34d399-fg}${task}{/} {#718096-fg}${stage}{/} {#34d399-fg}${model}{/} {#718096-fg}${tokens}{/} {${color}-fg}${icon} ${ev.status}{/}`);
507
- }
508
- box.setContent(lines.join('\n'));
509
- }
510
- catch (err) {
511
- box.setContent(`\n{center}{#ef4444-fg}Failed to load pipeline{/}\n{#718096-fg}${err}{/}{/center}`);
512
- }
513
- }
514
- async function loadStuckData(box) {
515
- try {
516
- const response = await fetch('http://127.0.0.1:3847/api/stuck-issues');
517
- const { stuckIssues, failedIssues } = await response.json();
518
- const totalStuck = stuckIssues.length;
519
- const totalFailed = failedIssues.length;
520
- const total = totalStuck + totalFailed;
521
- if (total === 0) {
522
- box.setContent('\n{center}{#34d399-fg}✓ All issues healthy{/}{/center}');
523
- return;
524
- }
525
- const lines = [
526
- '',
527
- ` {#ef4444-fg}⚠ ${total} issue${total > 1 ? 's' : ''} need attention{/}`,
528
- '',
529
- ];
530
- // Stuck issues
531
- if (totalStuck > 0) {
532
- lines.push(` {#f59e0b-fg}{bold}⏱ STUCK (${totalStuck}){/bold}{/}`);
533
- lines.push('');
534
- for (const issue of stuckIssues) {
535
- const priorityIcon = issue.priority === 1 ? '{#ef4444-fg}🔴{/}' : issue.priority === 2 ? '{#f59e0b-fg}🟡{/}' : '{#718096-fg}⚪{/}';
536
- lines.push(` ${priorityIcon} {bold}${issue.identifier}{/bold}`);
537
- lines.push(` ${issue.title.substring(0, 60)}${issue.title.length > 60 ? '...' : ''}`);
538
- lines.push(` {#f59e0b-fg}${issue.reason}{/}`);
539
- if (issue.project?.name) {
540
- lines.push(` {#718096-fg}📁 ${issue.project.name}{/}`);
541
- }
542
- lines.push('');
543
- }
544
- }
545
- // Failed issues
546
- if (totalFailed > 0) {
547
- lines.push(` {#ef4444-fg}{bold}✖ FAILED (${totalFailed}){/bold}{/}`);
548
- lines.push('');
549
- for (const issue of failedIssues) {
550
- const priorityIcon = issue.priority === 1 ? '{#ef4444-fg}🔴{/}' : issue.priority === 2 ? '{#f59e0b-fg}🟡{/}' : '{#718096-fg}⚪{/}';
551
- lines.push(` ${priorityIcon} {bold}${issue.identifier}{/bold}`);
552
- lines.push(` ${issue.title.substring(0, 60)}${issue.title.length > 60 ? '...' : ''}`);
553
- lines.push(` {#ef4444-fg}${issue.reason}{/}`);
554
- if (issue.project?.name) {
555
- lines.push(` {#718096-fg}📁 ${issue.project.name}{/}`);
556
- }
557
- lines.push('');
558
- }
559
- }
560
- box.setContent(lines.join('\n'));
561
- }
562
- catch (err) {
563
- box.setContent(`\n{center}{#ef4444-fg}Failed to load stuck issues{/}\n{#718096-fg}${err}{/}{/center}`);
564
- }
565
- }
566
- // Issues Data Loader (로컬 이슈 트래커)
567
- async function loadIssuesData(box) {
568
- try {
569
- const response = await fetch('http://127.0.0.1:3847/graphql', {
570
- method: 'POST',
571
- headers: { 'Content-Type': 'application/json' },
572
- body: JSON.stringify({
573
- query: `{
574
- issues(filter: { limit: 50 }) {
575
- issues { id title status priority projectId assignee labels updatedAt }
576
- total
577
- }
578
- issueStats {
579
- total
580
- byStatus { status count }
581
- recentlyCreated
582
- recentlyClosed
583
- }
584
- }`,
585
- }),
586
- });
587
- const json = await response.json();
588
- if (json.errors)
589
- throw new Error(json.errors[0].message);
590
- const { issues: { issues, total }, issueStats } = json.data;
591
- if (total === 0) {
592
- box.setContent('\n{center}{#718096-fg}No issues tracked{/}\n\n{#445544-fg}Create issues via web dashboard (:3847/issues){/}{/center}');
593
- return;
594
- }
595
- const lines = [
596
- '',
597
- ` {#00ccdd-fg}{bold}🎫 ISSUES{/bold}{/} — total: {bold}${issueStats.total}{/bold} new(7d): {#00ff41-fg}${issueStats.recentlyCreated}{/} closed(7d): {#ffaa00-fg}${issueStats.recentlyClosed}{/}`,
598
- '',
599
- ];
600
- // 상태별 요약
601
- const statusLine = issueStats.byStatus
602
- .map((s) => {
603
- const colors = {
604
- backlog: '#718096', todo: '#e2e8f0', in_progress: '#ffaa00',
605
- in_review: '#00ccdd', done: '#00ff41', cancelled: '#ef4444',
606
- };
607
- return `{${colors[s.status] || '#718096'}-fg}${s.status}: ${s.count}{/}`;
608
- })
609
- .join(' ');
610
- lines.push(` ${statusLine}`);
611
- lines.push(' ' + '─'.repeat(70));
612
- lines.push('');
613
- // 이슈 목록
614
- const priorityIcons = {
615
- urgent: '{#ef4444-fg}●{/}', high: '{#ffaa00-fg}●{/}',
616
- medium: '{#00ccdd-fg}●{/}', low: '{#718096-fg}●{/}', none: '{#445544-fg}○{/}',
617
- };
618
- const statusColors = {
619
- backlog: '#718096', todo: '#e2e8f0', in_progress: '#ffaa00',
620
- in_review: '#00ccdd', done: '#00ff41', cancelled: '#ef4444',
621
- };
622
- for (const iss of issues) {
623
- const icon = priorityIcons[iss.priority] || '{#718096-fg}●{/}';
624
- const stColor = statusColors[iss.status] || '#718096';
625
- const title = iss.title.length > 50 ? iss.title.substring(0, 50) + '...' : iss.title;
626
- const labels = (iss.labels || []).slice(0, 2).map((l) => `{#445544-fg}[${l}]{/}`).join('');
627
- lines.push(` ${icon} {${stColor}-fg}${iss.status.padEnd(12)}{/} {bold}${title}{/bold}`);
628
- lines.push(` {#718096-fg}${iss.id.slice(0, 6)} | ${iss.projectId}${iss.assignee ? ' | ' + iss.assignee : ''}{/} ${labels}`);
629
- lines.push('');
630
- }
631
- if (total > 50) {
632
- lines.push(` {#718096-fg}... and ${total - 50} more issues{/}`);
633
- }
634
- lines.push('');
635
- lines.push(' {#445544-fg}Open web dashboard for full management: http://localhost:3847/issues{/}');
636
- box.setContent(lines.join('\n'));
637
- }
638
- catch (err) {
639
- box.setContent(`\n{center}{#ef4444-fg}Failed to load issues{/}\n{#718096-fg}${err}{/}\n\n{#445544-fg}Make sure the service is running{/}{/center}`);
640
- }
641
- }
642
- // Loading Spinner (inline in chat)
643
- function startSpinner(ui) {
644
- let frameIndex = 0;
645
- const loadingMessage = LOADING_MESSAGES[Math.floor(Math.random() * LOADING_MESSAGES.length)];
646
- const lines = ui.chatLog.getLines();
647
- const lineIndex = lines.length;
648
- const interval = setInterval(() => {
649
- const spinner = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
650
- const content = ` {#667eea-fg}${spinner}{/} {#718096-fg}${loadingMessage}...{/}`;
651
- ui.chatLog.setLine(lineIndex, content);
652
- ui.chatLog.setScrollPerc(100);
653
- safeRender();
654
- frameIndex++;
655
- }, 80);
656
- return { interval, lineIndex };
657
- }
658
- function stopSpinner(ui, spinnerData) {
659
- clearInterval(spinnerData.interval);
660
- ui.chatLog.deleteLine(spinnerData.lineIndex);
661
- safeRender();
662
- }
663
- // Chat Logic
664
- async function sendMessage(state, ui, message, opts) {
665
- if (!message.trim())
666
- return;
667
- ui.chatLog.log('');
668
- ui.chatLog.log(`{#60a5fa-fg}{bold}▸ You{/bold}{/}`);
669
- ui.chatLog.log(` ${message}`);
670
- ui.chatLog.log('');
671
- ui.chatLog.setScrollPerc(100);
672
- safeRender();
673
- state.session.messages.push({ role: 'user', content: message });
674
- ui.chatLog.log(`{#34d399-fg}{bold}▸ Assistant{/bold}{/}`);
675
- const assistantHeaderLine = ui.chatLog.getLines().length - 1;
676
- let assistantContent = '';
677
- let lastRenderTime = 0;
678
- let spinnerStopped = false;
679
- let contentStartLine = assistantHeaderLine + 1;
680
- const spinnerData = startSpinner(ui);
681
- const controller = new AbortController();
682
- state.activeRun = controller;
683
- try {
684
- const result = await callChatModel(message, state.session.provider, state.session.model, (chunk, isThinking) => {
685
- // Handle thinking notification (show/resume spinner)
686
- if (isThinking) {
687
- if (spinnerStopped) {
688
- // Resume spinner for thinking phase
689
- spinnerStopped = false;
690
- const newSpinner = startSpinner(ui);
691
- Object.assign(spinnerData, newSpinner);
692
- }
693
- return;
694
- }
695
- // Stop spinner on first text chunk
696
- if (!spinnerStopped && chunk) {
697
- stopSpinner(ui, spinnerData);
698
- spinnerStopped = true;
699
- }
700
- if (!chunk)
701
- return;
702
- assistantContent += chunk;
703
- // Throttle rendering for smoother streaming (30fps)
704
- const now = Date.now();
705
- if (now - lastRenderTime < 33)
706
- return;
707
- lastRenderTime = now;
708
- // Clear previous content lines (안전한 역순 삭제)
709
- const currentLines = ui.chatLog.getLines().length;
710
- const deleteCount = Math.max(0, Math.min(currentLines - contentStartLine, currentLines));
711
- for (let i = 0; i < deleteCount; i++) {
712
- ui.chatLog.deleteLine(contentStartLine);
713
- }
714
- // Add updated content line by line with proper empty line handling
715
- const contentLines = assistantContent.split('\n');
716
- for (const line of contentLines) {
717
- // Always add line, even if empty (preserves paragraph breaks)
718
- ui.chatLog.log(line ? ` ${line}` : ' ');
719
- }
720
- ui.chatLog.setScrollPerc(100);
721
- safeRender();
722
- }, (toolLine) => {
723
- // Only surface tool executions (🔧 …); skip API-call markers and the
724
- // adapter summary line (cost is shown in the footer). Commit the current
725
- // streamed block, print the tool line, and move contentStartLine past it so
726
- // the 30fps re-render of the next text block doesn't erase it.
727
- if (!toolLine.includes('🔧'))
728
- return;
729
- if (!spinnerStopped) {
730
- stopSpinner(ui, spinnerData);
731
- spinnerStopped = true;
732
- }
733
- ui.chatLog.log(` {#fbbf24-fg}${toolLine.trim()}{/}`);
734
- assistantContent = '';
735
- contentStartLine = ui.chatLog.getLines().length;
736
- lastRenderTime = 0;
737
- ui.chatLog.setScrollPerc(100);
738
- safeRender();
739
- }, opts?.maxTurns, controller.signal);
740
- // Ensure spinner is stopped
741
- if (!spinnerStopped) {
742
- stopSpinner(ui, spinnerData);
743
- spinnerStopped = true;
744
- }
745
- // Update session stats
746
- state.session.totalCost += result.cost;
747
- state.session.totalTokens += result.tokens;
748
- // Finalize assistant message with cost
749
- // Clear streaming content first (안전한 삭제)
750
- const finalLines = ui.chatLog.getLines().length;
751
- const finalDeleteCount = Math.max(0, Math.min(finalLines - contentStartLine, finalLines));
752
- for (let i = 0; i < finalDeleteCount; i++) {
753
- ui.chatLog.deleteLine(contentStartLine);
754
- }
755
- // Add final content line by line with proper empty line handling
756
- const contentLines = result.response.split('\n');
757
- for (const line of contentLines) {
758
- // Always add line, even if empty (preserves paragraph breaks)
759
- ui.chatLog.log(line ? ` ${line}` : ' ');
760
- }
761
- // Add cost info if available
762
- if (result.cost > 0) {
763
- ui.chatLog.log(` {#718096-fg}${result.tokens} tokens · $${result.cost.toFixed(4)}{/}`);
764
- }
765
- ui.chatLog.log('');
766
- state.session.messages.push({
767
- role: 'assistant',
768
- content: result.response,
769
- cost: result.cost,
770
- });
771
- await saveSession(state.session);
772
- updateStatusBar(state, ui);
773
- safeRender();
774
- }
775
- catch (err) {
776
- if (!spinnerStopped) {
777
- stopSpinner(ui, spinnerData);
778
- }
779
- const msg = err instanceof Error ? err.message : String(err);
780
- ui.chatLog.log(`{#ef4444-fg}{bold}✗ Error{/bold}{/}`);
781
- ui.chatLog.log(` ${msg}`);
782
- ui.chatLog.log('');
783
- state.session.messages.pop(); // Remove user message on failure
784
- safeRender();
785
- }
786
- finally {
787
- state.activeRun = undefined;
788
- }
789
- }
790
- // Status Bar Update
791
- function updateStatusBar(state, ui) {
792
- const modelShort = shortenChatModel(state.session.model);
793
- const cost = state.session.totalCost > 0 ? `$${state.session.totalCost.toFixed(4)}` : '$0.00';
794
- const msgs = state.session.messages.length;
795
- const status = [
796
- '{bold}OpenSwarm{/bold}',
797
- `{#718096-fg}│{/}`,
798
- `{#a0aec0-fg}${state.session.id}{/}`,
799
- `{#718096-fg}│{/}`,
800
- `{#c084fc-fg}${state.session.provider}{/}`,
801
- `{#718096-fg}│{/}`,
802
- `{#60a5fa-fg}${modelShort}{/}`,
803
- `{#718096-fg}│{/}`,
804
- `{#a0aec0-fg}${msgs} messages{/}`,
805
- `{#718096-fg}│{/}`,
806
- `{#34d399-fg}${cost}{/}`,
807
- ].join(' ');
808
- ui.statusBar.setContent(' ' + status);
809
- }
810
- // Command Handler
811
- async function handleCommand(cmd, state, ui) {
812
- const [command, ...args] = cmd.slice(1).split(' ');
813
- switch (command) {
814
- case 'clear':
815
- case 'c':
816
- state.session.messages = [];
817
- state.session.totalCost = 0;
818
- state.session.totalTokens = 0;
819
- ui.chatLog.setContent('');
820
- ui.chatLog.log('');
821
- ui.chatLog.log('{#34d399-fg}✓ Conversation cleared{/}');
822
- ui.chatLog.log('');
823
- updateStatusBar(state, ui);
824
- safeRender();
825
- break;
826
- case 'provider':
827
- case 'p': {
828
- const next = args[0];
829
- ui.chatLog.log('');
830
- if (!next) {
831
- ui.chatLog.log(` {bold}Current provider:{/bold} {#c084fc-fg}${state.session.provider}{/}`);
832
- ui.chatLog.log(' {#718096-fg}Available providers:{/}');
833
- ui.chatLog.log(' {#a0aec0-fg}codex{/}');
834
- ui.chatLog.log(' {#a0aec0-fg}openrouter{/}');
835
- ui.chatLog.log(' {#a0aec0-fg}lmstudio{/}');
836
- ui.chatLog.log(' {#a0aec0-fg}local{/}');
837
- ui.chatLog.log(' {#a0aec0-fg}gpt{/}');
838
- }
839
- else {
840
- state.session.provider = next;
841
- state.session.model = getDefaultChatModel(state.session.provider);
842
- ui.chatLog.log(` {#34d399-fg}✓ Provider changed to {bold}${next}{/bold}{/}`);
843
- ui.chatLog.log(` {#34d399-fg}✓ Model changed to {bold}${state.session.model}{/bold}{/}`);
844
- updateStatusBar(state, ui);
845
- }
846
- ui.chatLog.log('');
847
- safeRender();
848
- break;
849
- }
850
- case 'model':
851
- case 'm': {
852
- const newModel = args[0];
853
- ui.chatLog.log('');
854
- if (!newModel) {
855
- ui.chatLog.log(` {bold}Current provider:{/bold} {#c084fc-fg}${state.session.provider}{/}`);
856
- ui.chatLog.log(` {bold}Current model:{/bold} {#60a5fa-fg}${shortenChatModel(state.session.model)}{/}`);
857
- ui.chatLog.log('');
858
- ui.chatLog.log(' {#718096-fg}Available models:{/}');
859
- if (state.session.provider === 'openrouter') {
860
- ui.chatLog.log(' {#a0aec0-fg}sonnet{/} {#718096-fg}→{/} anthropic/claude-sonnet-4');
861
- ui.chatLog.log(' {#a0aec0-fg}gemini{/} {#718096-fg}→{/} google/gemini-2.5-pro');
862
- ui.chatLog.log(' {#a0aec0-fg}gpt-5{/} {#718096-fg}→{/} openai/gpt-5');
863
- }
864
- else {
865
- ui.chatLog.log(' {#a0aec0-fg}codex{/} {#718096-fg}→{/} gpt-5-codex');
866
- }
867
- }
868
- else {
869
- state.session.model = resolveChatModel(newModel, state.session.provider);
870
- const shortName = shortenChatModel(state.session.model);
871
- ui.chatLog.log(` {#34d399-fg}✓ Model changed to {bold}${shortName}{/bold}{/}`);
872
- updateStatusBar(state, ui);
873
- }
874
- ui.chatLog.log('');
875
- safeRender();
876
- break;
877
- }
878
- case 'save': {
879
- const name = args[0] || state.session.id;
880
- state.session.id = name;
881
- await saveSession(state.session);
882
- ui.chatLog.log('');
883
- ui.chatLog.log(` {#34d399-fg}✓ Session saved: {bold}${name}{/bold}{/}`);
884
- ui.chatLog.log('');
885
- updateStatusBar(state, ui);
886
- safeRender();
887
- break;
888
- }
889
- case 'plan': {
890
- const goal = args.join(' ').trim();
891
- if (!goal) {
892
- ui.chatLog.log('');
893
- ui.chatLog.log(' {#fbbf24-fg}Usage: /plan <goal>{/}');
894
- ui.chatLog.log('');
895
- safeRender();
896
- break;
897
- }
898
- const io = {
899
- print: (line) => {
900
- ui.chatLog.log(line ? ` ${line}` : '');
901
- safeRender();
902
- },
903
- confirm: (prompt) => new Promise((resolve) => {
904
- ui.chatLog.log(` {#fbbf24-fg}${prompt}{/}`);
905
- safeRender();
906
- state.pendingInput = (v) => {
907
- const a = v.trim().toLowerCase();
908
- resolve(a === 'y' || a === 'yes' ? 'yes' : a === 'e' || a === 'edit' ? 'edit' : 'no');
909
- };
910
- }),
911
- promptText: (prompt) => new Promise((resolve) => {
912
- ui.chatLog.log(` {#fbbf24-fg}${prompt}{/}`);
913
- safeRender();
914
- state.pendingInput = (v) => resolve(v);
915
- }),
916
- };
917
- await runPlanCommand(goal, io, { projectPath: process.cwd() });
918
- break;
919
- }
920
- case 'goal': {
921
- const goalText = args.join(' ').trim();
922
- if (!goalText) {
923
- ui.chatLog.log('');
924
- ui.chatLog.log(state.session.goal
925
- ? ` {#fbbf24-fg}Current goal:{/} ${state.session.goal}`
926
- : ' {#fbbf24-fg}Usage: /goal <goal> — set a goal and pursue it autonomously{/}');
927
- ui.chatLog.log('');
928
- safeRender();
929
- break;
930
- }
931
- state.session.goal = goalText;
932
- await saveSession(state.session);
933
- updateStatusBar(state, ui);
934
- ui.chatLog.log('');
935
- ui.chatLog.log(` {#34d399-fg}{bold}Goal set — pursuing autonomously:{/bold}{/} ${goalText}`);
936
- ui.chatLog.log('');
937
- safeRender();
938
- const goalPrompt = `[GOAL] ${goalText}\n\nWork autonomously toward this goal: break it down, use your tools to implement and ` +
939
- `verify each step, narrate your reasoning as you go, and keep going until the goal is achieved or you are ` +
940
- `genuinely blocked. Do not ask for approval between steps.`;
941
- await sendMessage(state, ui, goalPrompt, { maxTurns: 120 });
942
- break;
943
- }
944
- case 'export': {
945
- const arg = args.join(' ').trim();
946
- const now = new Date();
947
- const pad = (n) => String(n).padStart(2, '0');
948
- const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
949
- const outPath = arg
950
- ? expandPath(arg)
951
- : resolve(process.cwd(), `openswarm-chat-${state.session.id}-${stamp}.txt`);
952
- const lines = [
953
- `# OpenSwarm Chat — session "${state.session.id}"`,
954
- `# ${state.session.provider}:${state.session.model} · ${now.toISOString()}`,
955
- ...(state.session.goal ? [`# Goal: ${state.session.goal}`] : []),
956
- '',
957
- ];
958
- for (const m of state.session.messages) {
959
- lines.push(m.role === 'user' ? 'You:' : 'Assistant:', m.content, '');
960
- }
961
- ui.chatLog.log('');
962
- try {
963
- await writeFile(outPath, lines.join('\n'), 'utf-8');
964
- ui.chatLog.log(` {#34d399-fg}Exported ${state.session.messages.length} messages → ${outPath}{/}`);
965
- }
966
- catch (err) {
967
- ui.chatLog.log(` {#ef4444-fg}Export failed: ${err instanceof Error ? err.message : String(err)}{/}`);
968
- }
969
- ui.chatLog.log('');
970
- safeRender();
971
- break;
972
- }
973
- case 'help':
974
- case 'h':
975
- case '?':
976
- ui.chatLog.log('');
977
- ui.chatLog.log(' {bold}Available Commands{/bold}');
978
- ui.chatLog.log('');
979
- ui.chatLog.log(' {#60a5fa-fg}/goal{/} <goal> Set a goal & pursue it autonomously (this session)');
980
- ui.chatLog.log(' {#60a5fa-fg}/plan{/} <goal> Decompose a goal & dispatch it to the loop');
981
- ui.chatLog.log(' {#60a5fa-fg}/clear{/} Clear conversation');
982
- ui.chatLog.log(' {#60a5fa-fg}/provider{/} [id] Change provider {#718096-fg}(claude/codex){/}');
983
- ui.chatLog.log(' {#60a5fa-fg}/model{/} [name] Change model {#718096-fg}(sonnet/haiku/opus){/}');
984
- ui.chatLog.log(' {#60a5fa-fg}/save{/} [name] Save session');
985
- ui.chatLog.log(' {#60a5fa-fg}/export{/} [path] Export conversation to a .txt file');
986
- ui.chatLog.log(' {#60a5fa-fg}/help{/} Show this help');
987
- ui.chatLog.log('');
988
- ui.chatLog.log(' {bold}Navigation{/bold}');
989
- ui.chatLog.log('');
990
- ui.chatLog.log(' {#718096-fg}1-4{/} Switch tabs directly');
991
- ui.chatLog.log(' {#718096-fg}Tab/Shift+Tab{/} Cycle through tabs');
992
- ui.chatLog.log(' {#718096-fg}Esc{/} Exit input mode (blur)');
993
- ui.chatLog.log(' {#718096-fg}i / Enter{/} Focus input (from chat)');
994
- ui.chatLog.log(' {#718096-fg}Ctrl+C{/} Exit (double press to confirm)');
995
- ui.chatLog.log('');
996
- safeRender();
997
- break;
998
- default:
999
- ui.chatLog.log('');
1000
- ui.chatLog.log(` {#ef4444-fg}Unknown command: /{bold}${command}{/bold}{/}`);
1001
- ui.chatLog.log(` {#718096-fg}Type {/}{#60a5fa-fg}/help{/}{#718096-fg} for available commands{/}`);
1002
- ui.chatLog.log('');
1003
- safeRender();
1004
- }
1005
- return false;
1006
- }
1007
- // Main
1008
- export async function main() {
1009
- const defaultProvider = loadDefaultProvider();
1010
- const loadArg = process.argv[2];
1011
- let session;
1012
- if (loadArg && loadArg !== '--' && !loadArg.startsWith('-')) {
1013
- const loaded = await loadSession(loadArg);
1014
- if (loaded) {
1015
- session = loaded;
1016
- }
1017
- else {
1018
- session = {
1019
- id: loadArg,
1020
- provider: defaultProvider,
1021
- model: getDefaultChatModel(defaultProvider),
1022
- messages: [],
1023
- totalCost: 0,
1024
- totalTokens: 0,
1025
- createdAt: new Date().toISOString(),
1026
- updatedAt: new Date().toISOString(),
1027
- };
1028
- }
1029
- }
1030
- else {
1031
- session = {
1032
- id: generateSessionId(),
1033
- provider: defaultProvider,
1034
- model: getDefaultChatModel(defaultProvider),
1035
- messages: [],
1036
- totalCost: 0,
1037
- totalTokens: 0,
1038
- createdAt: new Date().toISOString(),
1039
- updatedAt: new Date().toISOString(),
1040
- };
1041
- }
1042
- const state = {
1043
- session,
1044
- currentTab: 0,
1045
- inputMode: 'normal',
1046
- multilineBuffer: [],
1047
- showBinary: false,
1048
- diagnostics: {
1049
- lastResponseTime: 0,
1050
- avgTokensPerSec: 0,
1051
- totalRequests: 0,
1052
- },
1053
- };
1054
- const ui = createUI();
1055
- screenRef = ui.screen; // safeRender용 참조 설정
1056
- updateStatusBar(state, ui);
1057
- updateTabBar(ui, state.currentTab);
1058
- // Restore chat history - Claude Code style with proper line breaks
1059
- for (const msg of session.messages) {
1060
- ui.chatLog.log('');
1061
- if (msg.role === 'user') {
1062
- ui.chatLog.log(`{#60a5fa-fg}{bold}▸ You{/bold}{/}`);
1063
- // Split multiline user messages
1064
- const userLines = msg.content.split('\n');
1065
- for (const line of userLines) {
1066
- ui.chatLog.log(line ? ` ${line}` : ' ');
1067
- }
1068
- }
1069
- else {
1070
- ui.chatLog.log(`{#34d399-fg}{bold}▸ Assistant{/bold}{/}`);
1071
- // Split multiline assistant messages properly
1072
- const assistantLines = msg.content.split('\n');
1073
- for (const line of assistantLines) {
1074
- ui.chatLog.log(line ? ` ${line}` : ' ');
1075
- }
1076
- if (msg.cost) {
1077
- ui.chatLog.log(` {#718096-fg}$${msg.cost.toFixed(4)}{/}`);
1078
- }
1079
- }
1080
- }
1081
- if (session.messages.length > 0) {
1082
- ui.chatLog.log('');
1083
- }
1084
- // Key bindings
1085
- ui.screen.key(['1'], () => switchTab(state, ui, 0));
1086
- ui.screen.key(['2'], () => switchTab(state, ui, 1));
1087
- ui.screen.key(['3'], () => switchTab(state, ui, 2));
1088
- ui.screen.key(['4'], () => switchTab(state, ui, 3));
1089
- ui.screen.key(['5'], () => switchTab(state, ui, 4));
1090
- ui.screen.key(['tab'], () => {
1091
- const next = (state.currentTab + 1) % 6;
1092
- switchTab(state, ui, next);
1093
- });
1094
- ui.screen.key(['S-tab'], () => {
1095
- const prev = (state.currentTab - 1 + 6) % 6;
1096
- switchTab(state, ui, prev);
1097
- });
1098
- // Ctrl+C: Clear input or exit (Claude Code style)
1099
- let ctrlCPressed = false;
1100
- ui.screen.key(['C-c'], async () => {
1101
- // While an agent run is in flight, stop it (don't clear input / exit).
1102
- if (state.activeRun) {
1103
- state.activeRun.abort();
1104
- ui.chatLog.log(' {#f59e0b-fg}■ Stopped{/}');
1105
- safeRender();
1106
- return;
1107
- }
1108
- const currentValue = ui.inputBox.getValue();
1109
- if (currentValue && currentValue.trim()) {
1110
- // If input has text, just clear it
1111
- ui.inputBox.clearValue();
1112
- ui.inputBox.focus();
1113
- ui.commandPalette.hide();
1114
- safeRender();
1115
- ctrlCPressed = false;
1116
- }
1117
- else {
1118
- // If input is empty, exit with double Ctrl+C
1119
- if (ctrlCPressed) {
1120
- await saveSession(state.session);
1121
- process.exit(0);
1122
- }
1123
- else {
1124
- ctrlCPressed = true;
1125
- ui.statusBar.setContent(' {#f59e0b-fg}Press Ctrl+C again to exit{/}');
1126
- safeRender();
1127
- setTimeout(() => {
1128
- ctrlCPressed = false;
1129
- updateStatusBar(state, ui);
1130
- safeRender();
1131
- }, 2000);
1132
- }
1133
- }
1134
- });
1135
- // Escape: Clear input and blur (exit input mode)
1136
- ui.screen.key(['escape'], () => {
1137
- // While an agent run is in flight, stop it (don't clear input / blur).
1138
- if (state.activeRun) {
1139
- state.activeRun.abort();
1140
- ui.chatLog.log(' {#f59e0b-fg}■ Stopped{/}');
1141
- safeRender();
1142
- return;
1143
- }
1144
- const currentValue = ui.inputBox.getValue();
1145
- if (currentValue && currentValue.trim()) {
1146
- // Clear input if has content
1147
- ui.inputBox.clearValue();
1148
- }
1149
- // Blur input box to exit input mode
1150
- ui.chatLog.focus();
1151
- ui.commandPalette.hide();
1152
- safeRender();
1153
- });
1154
- // Enter in chatLog: focus input
1155
- ui.chatLog.key(['enter', 'i'], () => {
1156
- ui.inputBox.focus();
1157
- safeRender();
1158
- });
1159
- // Slash-command palette: refresh on every keystroke in the input box.
1160
- ui.inputBox.on('keypress', () => {
1161
- setImmediate(() => updateCommandPalette(ui));
1162
- });
1163
- // Shift+Enter: Insert newline (handled by textarea by default)
1164
- // Enter: Submit message
1165
- ui.inputBox.key(['enter'], async () => {
1166
- const value = ui.inputBox.getValue();
1167
- const trimmed = value.trim();
1168
- if (!trimmed)
1169
- return;
1170
- ui.inputBox.clearValue();
1171
- ui.inputBox.focus();
1172
- ui.commandPalette.hide();
1173
- safeRender();
1174
- // An in-progress /plan approval consumes the next line, not chat/commands.
1175
- if (state.pendingInput) {
1176
- const resolve = state.pendingInput;
1177
- state.pendingInput = undefined;
1178
- resolve(trimmed);
1179
- return;
1180
- }
1181
- if (trimmed.startsWith('/')) {
1182
- await handleCommand(trimmed, state, ui);
1183
- }
1184
- else {
1185
- await sendMessage(state, ui, trimmed);
1186
- }
1187
- });
1188
- // Focus input by default
1189
- ui.inputBox.focus();
1190
- // Handle terminal resize (important for tmux) — debounce로 연속 resize 시 깜빡임 방지
1191
- let resizeTimer = null;
1192
- process.stdout.on('resize', () => {
1193
- if (resizeTimer)
1194
- clearTimeout(resizeTimer);
1195
- resizeTimer = setTimeout(() => {
1196
- ui.screen.alloc();
1197
- ui.screen.realloc();
1198
- safeRender();
1199
- }, 50);
1200
- });
1201
- // Render
1202
- safeRender();
1203
- // Auto-refresh Projects/Tasks/Stuck tabs every 5s
1204
- setInterval(() => {
1205
- if (state.currentTab === 1)
1206
- loadProjectsData(ui.projectsBox);
1207
- if (state.currentTab === 2)
1208
- loadTasksData(ui.tasksBox);
1209
- if (state.currentTab === 3)
1210
- loadStuckData(ui.stuckBox);
1211
- if (state.currentTab === 4)
1212
- loadIssuesData(ui.issuesBox);
1213
- safeRender();
1214
- }, 5000);
1215
- // System logs (example - hook into eventHub in real implementation)
1216
- ui.logsBox.log('{gray-fg}System initialized{/gray-fg}');
1217
- }
1218
- // Auto-run if called directly
1219
- if (import.meta.url === `file://${process.argv[1]}`) {
1220
- main().catch((err) => {
1221
- console.error('Fatal:', err);
1222
- process.exit(1);
1223
- });
1224
- }
1225
- //# sourceMappingURL=chatTui.js.map