gencode-ai 0.3.0 → 0.4.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 (116) hide show
  1. package/RELEASE_NOTES_v0.4.0.md +140 -0
  2. package/dist/agent/agent.d.ts +17 -2
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +279 -49
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +15 -1
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/checkpointing/checkpoint-manager.d.ts +24 -0
  9. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -1
  10. package/dist/checkpointing/checkpoint-manager.js +28 -0
  11. package/dist/checkpointing/checkpoint-manager.js.map +1 -1
  12. package/dist/cli/components/App.d.ts +8 -0
  13. package/dist/cli/components/App.d.ts.map +1 -1
  14. package/dist/cli/components/App.js +478 -36
  15. package/dist/cli/components/App.js.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  17. package/dist/cli/components/CommandSuggestions.js +2 -0
  18. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  19. package/dist/cli/components/Header.d.ts +6 -1
  20. package/dist/cli/components/Header.d.ts.map +1 -1
  21. package/dist/cli/components/Header.js +3 -3
  22. package/dist/cli/components/Header.js.map +1 -1
  23. package/dist/cli/components/Messages.d.ts.map +1 -1
  24. package/dist/cli/components/Messages.js +7 -9
  25. package/dist/cli/components/Messages.js.map +1 -1
  26. package/dist/cli/index.js +3 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/config/types.d.ts +20 -1
  29. package/dist/config/types.d.ts.map +1 -1
  30. package/dist/config/types.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +2 -2
  33. package/dist/input/history-manager.d.ts +78 -0
  34. package/dist/input/history-manager.d.ts.map +1 -0
  35. package/dist/input/history-manager.js +224 -0
  36. package/dist/input/history-manager.js.map +1 -0
  37. package/dist/input/index.d.ts +6 -0
  38. package/dist/input/index.d.ts.map +1 -0
  39. package/dist/input/index.js +5 -0
  40. package/dist/input/index.js.map +1 -0
  41. package/dist/prompts/index.js +3 -3
  42. package/dist/prompts/index.js.map +1 -1
  43. package/dist/providers/gemini.d.ts.map +1 -1
  44. package/dist/providers/gemini.js +33 -2
  45. package/dist/providers/gemini.js.map +1 -1
  46. package/dist/providers/google.d.ts +22 -0
  47. package/dist/providers/google.d.ts.map +1 -0
  48. package/dist/providers/google.js +297 -0
  49. package/dist/providers/google.js.map +1 -0
  50. package/dist/providers/index.d.ts +4 -4
  51. package/dist/providers/index.js +11 -11
  52. package/dist/providers/index.js.map +1 -1
  53. package/dist/providers/openai.d.ts.map +1 -1
  54. package/dist/providers/openai.js +6 -0
  55. package/dist/providers/openai.js.map +1 -1
  56. package/dist/providers/registry.js +3 -3
  57. package/dist/providers/registry.js.map +1 -1
  58. package/dist/providers/types.d.ts +30 -4
  59. package/dist/providers/types.d.ts.map +1 -1
  60. package/dist/session/compression/engine.d.ts +109 -0
  61. package/dist/session/compression/engine.d.ts.map +1 -0
  62. package/dist/session/compression/engine.js +311 -0
  63. package/dist/session/compression/engine.js.map +1 -0
  64. package/dist/session/compression/index.d.ts +12 -0
  65. package/dist/session/compression/index.d.ts.map +1 -0
  66. package/dist/session/compression/index.js +11 -0
  67. package/dist/session/compression/index.js.map +1 -0
  68. package/dist/session/compression/types.d.ts +90 -0
  69. package/dist/session/compression/types.d.ts.map +1 -0
  70. package/dist/session/compression/types.js +17 -0
  71. package/dist/session/compression/types.js.map +1 -0
  72. package/dist/session/manager.d.ts +64 -3
  73. package/dist/session/manager.d.ts.map +1 -1
  74. package/dist/session/manager.js +254 -2
  75. package/dist/session/manager.js.map +1 -1
  76. package/dist/session/types.d.ts +16 -0
  77. package/dist/session/types.d.ts.map +1 -1
  78. package/dist/session/types.js.map +1 -1
  79. package/docs/README.md +1 -0
  80. package/docs/diagrams/compression-decision.mmd +30 -0
  81. package/docs/diagrams/compression-workflow.mmd +54 -0
  82. package/docs/diagrams/layer1-pruning.mmd +45 -0
  83. package/docs/diagrams/layer2-compaction.mmd +42 -0
  84. package/docs/proposals/0007-context-management.md +252 -2
  85. package/docs/proposals/README.md +4 -3
  86. package/docs/providers.md +3 -3
  87. package/docs/session-compression.md +695 -0
  88. package/examples/agent-demo.ts +23 -1
  89. package/examples/basic.ts +3 -3
  90. package/package.json +4 -5
  91. package/src/agent/agent.ts +314 -52
  92. package/src/agent/types.ts +19 -1
  93. package/src/checkpointing/checkpoint-manager.ts +48 -0
  94. package/src/cli/components/App.tsx +553 -34
  95. package/src/cli/components/CommandSuggestions.tsx +2 -0
  96. package/src/cli/components/Header.tsx +16 -1
  97. package/src/cli/components/Messages.tsx +20 -14
  98. package/src/cli/index.tsx +3 -2
  99. package/src/config/types.ts +26 -1
  100. package/src/index.ts +3 -3
  101. package/src/input/history-manager.ts +289 -0
  102. package/src/input/index.ts +6 -0
  103. package/src/prompts/index.test.ts +2 -1
  104. package/src/prompts/index.ts +3 -3
  105. package/src/providers/{gemini.ts → google.ts} +69 -18
  106. package/src/providers/index.ts +14 -14
  107. package/src/providers/openai.ts +7 -0
  108. package/src/providers/registry.ts +3 -3
  109. package/src/providers/types.ts +33 -3
  110. package/src/session/compression/engine.ts +406 -0
  111. package/src/session/compression/index.ts +18 -0
  112. package/src/session/compression/types.ts +102 -0
  113. package/src/session/manager.ts +326 -3
  114. package/src/session/types.ts +21 -0
  115. package/tests/input-history-manager.test.ts +335 -0
  116. package/tests/session-checkpoint-persistence.test.ts +198 -0
@@ -36,12 +36,15 @@ import { getTodos, formatAnswersForDisplay } from '../../tools/index.js';
36
36
  import type { Question, QuestionAnswer } from '../../tools/types.js';
37
37
  import type { ProviderName } from '../../providers/index.js';
38
38
  import type { ApprovalAction, ApprovalSuggestion } from '../../permissions/types.js';
39
+ import type { Message, ToolResultContent, ToolUseContent } from '../../providers/types.js';
40
+ import type { SessionMetadata } from '../../session/types.js';
39
41
  import { gatherContextFiles, buildInitPrompt, getContextSummary } from '../../memory/index.js';
40
42
  // ModeIndicator kept for potential future use
41
43
  import { PlanApproval } from './PlanApproval.js';
42
44
  import type { ModeType, PlanApprovalOption, AllowedPrompt } from '../../planning/types.js';
43
45
  // Planning utilities kept for potential future use
44
46
  import { getCheckpointManager } from '../../checkpointing/index.js';
47
+ import { InputHistoryManager } from '../../input/index.js';
45
48
 
46
49
  // Types
47
50
  interface HistoryItem {
@@ -75,6 +78,7 @@ interface SettingsManager {
75
78
  save: (settings: { model?: string }) => Promise<void>;
76
79
  getCwd?: () => string;
77
80
  addPermissionRule?: (pattern: string, type: 'allow' | 'deny', level?: 'global' | 'project' | 'local') => Promise<void>;
81
+ get?: () => { inputHistory?: { enabled?: boolean; maxSize?: number; savePath?: string; deduplicateConsecutive?: boolean } };
78
82
  }
79
83
 
80
84
  interface Session {
@@ -106,17 +110,20 @@ function useAgent(config: AgentConfig) {
106
110
  // ============================================================================
107
111
  // Utils
108
112
  // ============================================================================
109
- const genId = () => Math.random().toString(36).slice(2);
113
+ function genId(): string {
114
+ return Math.random().toString(36).slice(2);
115
+ }
110
116
 
111
- const formatRelativeTime = (dateStr: string) => {
117
+ function formatRelativeTime(dateStr: string): string {
112
118
  const diff = Date.now() - new Date(dateStr).getTime();
113
119
  const mins = Math.floor(diff / 60000);
114
120
  const hrs = Math.floor(mins / 60);
115
121
  const days = Math.floor(hrs / 24);
122
+
116
123
  if (mins < 60) return `${mins}m`;
117
124
  if (hrs < 24) return `${hrs}h`;
118
125
  return `${days}d`;
119
- };
126
+ }
120
127
 
121
128
  // ============================================================================
122
129
  // Help Component
@@ -137,6 +144,8 @@ function HelpPanel() {
137
144
  ['/memory', 'Show memory files'],
138
145
  ['/changes', 'List file changes'],
139
146
  ['/rewind [n|all]', 'Undo file changes'],
147
+ ['/context', 'Show context stats'],
148
+ ['/compact', 'Compact conversation'],
140
149
  ];
141
150
 
142
151
  return (
@@ -224,6 +233,30 @@ function MemoryFilesDisplay({ files }: { files: MemoryFileInfo[] }) {
224
233
  );
225
234
  }
226
235
 
236
+ // ============================================================================
237
+ // Token Estimation Utilities
238
+ // ============================================================================
239
+
240
+ /**
241
+ * Language-aware token estimation for streaming text
242
+ * Provides better estimates for multilingual content than the simple 4:1 ratio
243
+ *
244
+ * @param text - Text chunk to estimate tokens for
245
+ * @returns Estimated token count
246
+ */
247
+ function estimateTokenDelta(text: string): number {
248
+ // ASCII/English: ~4 chars per token
249
+ const asciiChars = (text.match(/[\x00-\x7F]/g) || []).length;
250
+
251
+ // CJK (Chinese, Japanese, Korean): ~1.5 chars per token
252
+ const cjkChars = (text.match(/[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF]/g) || []).length;
253
+
254
+ // Other Unicode (including emojis): ~3 chars per token
255
+ const otherChars = text.length - asciiChars - cjkChars;
256
+
257
+ return Math.max(1, Math.ceil(asciiChars / 4 + cjkChars / 1.5 + otherChars / 3));
258
+ }
259
+
227
260
  // ============================================================================
228
261
  // Main App
229
262
  // ============================================================================
@@ -259,6 +292,14 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
259
292
  const [isThinking, setIsThinking] = useState(false);
260
293
  const [streamingText, setStreamingText] = useState('');
261
294
  const streamingTextRef = useRef(''); // Track current streaming text for closure
295
+
296
+ // Performance optimization: Throttle streaming text updates
297
+ const streamBufferRef = useRef(''); // Buffer for accumulated text
298
+ const lastFlushTimeRef = useRef(0); // Last time we flushed to UI
299
+ const flushTimerRef = useRef<NodeJS.Timeout | null>(null); // Pending flush timer
300
+ const FLUSH_INTERVAL_MS = 16; // ~60 FPS throttling
301
+
302
+ const [messageQueue, setMessageQueue] = useState<string[]>([]);
262
303
  const [processingStartTime, setProcessingStartTime] = useState<number | undefined>(undefined);
263
304
  const [tokenCount, setTokenCount] = useState(0);
264
305
  const [confirmState, setConfirmState] = useState<ConfirmState | null>(null);
@@ -273,6 +314,10 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
273
314
  const pendingToolRef = useRef<{ name: string; input: Record<string, unknown> } | null>(null);
274
315
  const [todos, setTodos] = useState<ReturnType<typeof getTodos>>([]);
275
316
 
317
+ // Input history management
318
+ const historyManagerRef = useRef<InputHistoryManager | null>(null);
319
+ const [historyTempInput, setHistoryTempInput] = useState(''); // Store original input when navigating
320
+
276
321
  // Operating mode state (normal → plan → accept → normal)
277
322
  const [currentMode, setCurrentMode] = useState<ModeType>('normal');
278
323
  const currentModeRef = useRef<ModeType>('normal'); // Track mode for confirm callback
@@ -292,11 +337,214 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
292
337
  setCmdSuggestionIndex(0);
293
338
  }, [input]);
294
339
 
340
+ // Initialize input history manager
341
+ useEffect(() => {
342
+ const initHistory = async () => {
343
+ const settings = settingsManager?.get?.();
344
+ const historyConfig = settings?.inputHistory;
345
+ const manager = new InputHistoryManager(historyConfig);
346
+ await manager.load();
347
+ historyManagerRef.current = manager;
348
+ };
349
+
350
+ initHistory();
351
+
352
+ // Cleanup: flush history on unmount
353
+ return () => {
354
+ if (historyManagerRef.current) {
355
+ void historyManagerRef.current.flush();
356
+ }
357
+ };
358
+ }, [settingsManager]);
359
+
295
360
  // Add to history
296
361
  const addHistory = useCallback((item: Omit<HistoryItem, 'id'>) => {
297
362
  setHistory((prev) => [...prev, { ...item, id: genId() }]);
298
363
  }, []);
299
364
 
365
+ // Track if warning has been shown (to avoid spam)
366
+ const contextWarningShownRef = useRef(false);
367
+
368
+ // Listen to session manager events for context warnings
369
+ useEffect(() => {
370
+ const sessionMgr = agent.getSessionManager();
371
+
372
+ const handleContextWarning = (data: { usagePercent: number }) => {
373
+ if (!contextWarningShownRef.current) {
374
+ addHistory({
375
+ type: 'info',
376
+ content: `⚠️ Context usage at ${Math.round(data.usagePercent)}% - Consider using /compact`,
377
+ });
378
+ contextWarningShownRef.current = true;
379
+ }
380
+ };
381
+
382
+ const handleAutoCompacting = (data: { strategy: string; usagePercent: number }) => {
383
+ addHistory({
384
+ type: 'info',
385
+ content: `📦 Auto-compacting (${Math.round(data.usagePercent)}% usage, strategy: ${data.strategy})...`,
386
+ });
387
+ };
388
+
389
+ const handleCompactionComplete = (data: { strategy: string }) => {
390
+ addHistory({
391
+ type: 'info',
392
+ content: `✓ Compaction complete (${data.strategy})`,
393
+ });
394
+ // Reset warning flag after compaction
395
+ contextWarningShownRef.current = false;
396
+ };
397
+
398
+ sessionMgr.on('context-warning', handleContextWarning);
399
+ sessionMgr.on('auto-compacting', handleAutoCompacting);
400
+ sessionMgr.on('compaction-complete', handleCompactionComplete);
401
+
402
+ return () => {
403
+ sessionMgr.off('context-warning', handleContextWarning);
404
+ sessionMgr.off('auto-compacting', handleAutoCompacting);
405
+ sessionMgr.off('compaction-complete', handleCompactionComplete);
406
+ };
407
+ }, [agent, addHistory]);
408
+
409
+ // Flush buffered streaming text to UI (throttled to ~60 FPS)
410
+ const flushStreamBuffer = useCallback(() => {
411
+ if (streamBufferRef.current) {
412
+ streamingTextRef.current = streamBufferRef.current;
413
+ setStreamingText(streamBufferRef.current);
414
+ lastFlushTimeRef.current = Date.now();
415
+ }
416
+ // Clear pending timer
417
+ if (flushTimerRef.current) {
418
+ clearTimeout(flushTimerRef.current);
419
+ flushTimerRef.current = null;
420
+ }
421
+ }, []);
422
+
423
+ // Add streaming text with throttling for performance
424
+ const addStreamingText = useCallback((text: string) => {
425
+ // Accumulate in buffer
426
+ streamBufferRef.current += text;
427
+
428
+ const now = Date.now();
429
+ const timeSinceLastFlush = now - lastFlushTimeRef.current;
430
+
431
+ if (timeSinceLastFlush >= FLUSH_INTERVAL_MS) {
432
+ // Flush immediately if enough time has passed
433
+ flushStreamBuffer();
434
+ } else if (!flushTimerRef.current) {
435
+ // Schedule a flush for the next interval
436
+ const delay = FLUSH_INTERVAL_MS - timeSinceLastFlush;
437
+ flushTimerRef.current = setTimeout(flushStreamBuffer, delay);
438
+ }
439
+ // Otherwise, wait for the scheduled flush
440
+ }, [flushStreamBuffer, FLUSH_INTERVAL_MS]);
441
+
442
+ // Convert Message[] to HistoryItem[] for displaying session history
443
+ const convertMessagesToHistory = useCallback((
444
+ messages: Message[],
445
+ metadata?: SessionMetadata
446
+ ): HistoryItem[] => {
447
+ const items: HistoryItem[] = [];
448
+
449
+ for (let i = 0; i < messages.length; i++) {
450
+ const msg = messages[i];
451
+
452
+ // Skip system messages (they're for the LLM, not for display)
453
+ if (msg.role === 'system') {
454
+ continue;
455
+ }
456
+
457
+ if (msg.role === 'user') {
458
+ // User messages can be plain text or contain tool results
459
+ if (typeof msg.content === 'string') {
460
+ items.push({
461
+ id: genId(),
462
+ type: 'user',
463
+ content: msg.content,
464
+ });
465
+ } else {
466
+ // Check for tool results
467
+ const toolResults = msg.content.filter((c) => c.type === 'tool_result');
468
+ const textContent = msg.content.filter((c) => c.type === 'text')
469
+ .map((c) => (c as { text: string }).text)
470
+ .join('\n');
471
+
472
+ if (textContent) {
473
+ items.push({
474
+ id: genId(),
475
+ type: 'user',
476
+ content: textContent,
477
+ });
478
+ }
479
+
480
+ // Add tool results
481
+ for (const result of toolResults) {
482
+ const r = result as ToolResultContent;
483
+ items.push({
484
+ id: genId(),
485
+ type: 'tool_result',
486
+ content: r.content,
487
+ meta: { toolUseId: r.toolUseId, isError: r.isError },
488
+ });
489
+ }
490
+ }
491
+ } else if (msg.role === 'assistant') {
492
+ // Assistant messages can be text or contain tool calls
493
+ if (typeof msg.content === 'string') {
494
+ items.push({
495
+ id: genId(),
496
+ type: 'assistant',
497
+ content: msg.content,
498
+ });
499
+ } else {
500
+ // Separate text and tool calls
501
+ const textContent = msg.content.filter((c) => c.type === 'text')
502
+ .map((c) => (c as { text: string }).text)
503
+ .join('\n');
504
+ const toolCalls = msg.content.filter((c) => c.type === 'tool_use');
505
+
506
+ if (textContent) {
507
+ items.push({
508
+ id: genId(),
509
+ type: 'assistant',
510
+ content: textContent,
511
+ });
512
+ }
513
+
514
+ // Add tool calls
515
+ for (const call of toolCalls) {
516
+ const c = call as ToolUseContent;
517
+ items.push({
518
+ id: genId(),
519
+ type: 'tool_call',
520
+ content: c.name,
521
+ meta: { id: c.id, name: c.name, input: c.input },
522
+ });
523
+ }
524
+ }
525
+ }
526
+
527
+ // Inject completion message if this message index has a completion
528
+ if (metadata?.completions) {
529
+ const completion = metadata.completions.find((c) => c.afterMessageIndex === i);
530
+ if (completion) {
531
+ items.push({
532
+ id: genId(),
533
+ type: 'completion',
534
+ content: '',
535
+ meta: {
536
+ durationMs: completion.durationMs,
537
+ usage: completion.usage,
538
+ cost: completion.cost,
539
+ },
540
+ });
541
+ }
542
+ }
543
+ }
544
+
545
+ return items;
546
+ }, []);
547
+
300
548
  // Initialize
301
549
  useEffect(() => {
302
550
  const init = async () => {
@@ -339,12 +587,18 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
339
587
  if (resumeLatest) {
340
588
  const resumed = await agent.resumeLatest();
341
589
  if (resumed) {
342
- addHistory({ type: 'info', content: 'Session restored' });
590
+ // Get the restored messages and display them
591
+ const messages = agent.getHistory();
592
+ const session = agent.getSessionManager().getCurrent();
593
+ const historyItems = convertMessagesToHistory(messages, session?.metadata);
594
+
595
+ // Add all historical messages
596
+ setHistory((prev) => [...prev, ...historyItems]);
343
597
  }
344
598
  }
345
599
  };
346
600
  init();
347
- }, [agent, resumeLatest, addHistory, permissionSettings, settingsManager]);
601
+ }, [agent, resumeLatest, addHistory, permissionSettings, settingsManager, convertMessagesToHistory]);
348
602
 
349
603
  // Handle question answers (AskUserQuestion)
350
604
  const handleQuestionComplete = useCallback((answers: QuestionAnswer[]) => {
@@ -432,20 +686,32 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
432
686
 
433
687
  case 'resume': {
434
688
  let success = false;
435
- if (arg) {
689
+
690
+ if (!arg) {
691
+ success = await agent.resumeLatest();
692
+ } else {
436
693
  const index = parseInt(arg, 10);
437
- if (!isNaN(index)) {
694
+ if (isNaN(index)) {
695
+ success = await agent.resumeSession(arg);
696
+ } else {
438
697
  const sessions = await agent.listSessions();
439
698
  if (index >= 1 && index <= sessions.length) {
440
699
  success = await agent.resumeSession(sessions[index - 1].id);
441
700
  }
442
- } else {
443
- success = await agent.resumeSession(arg);
444
701
  }
702
+ }
703
+
704
+ if (success) {
705
+ // Get the restored messages and display them
706
+ const messages = agent.getHistory();
707
+ const session = agent.getSessionManager().getCurrent();
708
+ const historyItems = convertMessagesToHistory(messages, session?.metadata);
709
+
710
+ // Add all historical messages
711
+ setHistory((prev) => [...prev, ...historyItems]);
445
712
  } else {
446
- success = await agent.resumeLatest();
713
+ addHistory({ type: 'info', content: 'Failed to restore session' });
447
714
  }
448
- addHistory({ type: 'info', content: success ? 'Restored' : 'Failed' });
449
715
  return true;
450
716
  }
451
717
 
@@ -711,6 +977,127 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
711
977
  return true;
712
978
  }
713
979
 
980
+ case 'compact': {
981
+ // Manually trigger conversation compaction
982
+ const sessionMgr = agent.getSessionManager();
983
+ const current = sessionMgr.getCurrent();
984
+
985
+ if (!current || current.messages.length === 0) {
986
+ addHistory({ type: 'info', content: 'No messages to compact' });
987
+ return true;
988
+ }
989
+
990
+ // Perform manual compaction
991
+ const modelInfo = agent.getModelInfo();
992
+ if (modelInfo) {
993
+ try {
994
+ await sessionMgr.performCompaction(modelInfo);
995
+
996
+ const stats = sessionMgr.getCompressionStats();
997
+ const saved = stats?.totalMessages ? stats.totalMessages - stats.activeMessages : 0;
998
+ const savedPercent = stats?.totalMessages
999
+ ? ((saved / stats.totalMessages) * 100).toFixed(0)
1000
+ : '0';
1001
+
1002
+ // Create visual bars (ASCII only)
1003
+ const barWidth = 15;
1004
+ const activeBar = stats?.totalMessages
1005
+ ? Math.round((stats.activeMessages / stats.totalMessages) * barWidth)
1006
+ : barWidth;
1007
+ const totalBar = barWidth;
1008
+
1009
+ const activeVisual = '#'.repeat(activeBar) + '.'.repeat(barWidth - activeBar);
1010
+ const totalVisual = '#'.repeat(totalBar);
1011
+
1012
+ // Format with simple ASCII box
1013
+ const w = 50;
1014
+ const pad = (text: string) => text + ' '.repeat(Math.max(0, w - text.length - 3));
1015
+
1016
+ const lines = [
1017
+ '+' + '-'.repeat(w - 2) + '+',
1018
+ '| ' + pad('Compaction Complete') + '|',
1019
+ '+' + '-'.repeat(w - 2) + '+',
1020
+ '| ' + pad(`Active Messages ${String(stats?.activeMessages || 0).padStart(3)} [${activeVisual}]`) + '|',
1021
+ '| ' + pad(`Total Messages ${String(stats?.totalMessages || 0).padStart(3)} [${totalVisual}]`) + '|',
1022
+ '| ' + pad(`Summaries ${String(stats?.summaryCount || 0).padStart(3)}`) + '|',
1023
+ '| ' + pad('') + '|',
1024
+ '| ' + pad(`Saved: ${savedPercent}%`) + '|',
1025
+ '+' + '-'.repeat(w - 2) + '+',
1026
+ ];
1027
+
1028
+ addHistory({
1029
+ type: 'info',
1030
+ content: '\n' + lines.join('\n'),
1031
+ });
1032
+ } catch (error) {
1033
+ addHistory({
1034
+ type: 'info',
1035
+ content: `Compaction failed: ${error instanceof Error ? error.message : String(error)}`,
1036
+ });
1037
+ }
1038
+ } else {
1039
+ addHistory({ type: 'info', content: 'Model information not available' });
1040
+ }
1041
+ return true;
1042
+ }
1043
+
1044
+ case 'context': {
1045
+ // Show context usage statistics
1046
+ const sessionMgr = agent.getSessionManager();
1047
+ const stats = sessionMgr.getCompressionStats();
1048
+
1049
+ if (!stats) {
1050
+ addHistory({ type: 'info', content: 'No compression statistics available' });
1051
+ return true;
1052
+ }
1053
+
1054
+ const activeRatio = stats.totalMessages > 0
1055
+ ? ((stats.activeMessages / stats.totalMessages) * 100).toFixed(0)
1056
+ : '100';
1057
+
1058
+ const isCompressed = stats.activeMessages < stats.totalMessages;
1059
+
1060
+ // Create progress bar (ASCII only)
1061
+ const barWidth = 20;
1062
+ const filledWidth = stats.totalMessages > 0
1063
+ ? Math.round((stats.activeMessages / stats.totalMessages) * barWidth)
1064
+ : barWidth;
1065
+ const progressBar = '#'.repeat(filledWidth) + '.'.repeat(barWidth - filledWidth);
1066
+
1067
+ const statusText = isCompressed ? 'Compressed' : 'Uncompressed';
1068
+ const statusColor = isCompressed ? '\x1b[32m' : '\x1b[90m';
1069
+
1070
+ // Format with simple ASCII box
1071
+ const w = 50;
1072
+ const visibleLength = (text: string) => text.replace(/\x1b\[[0-9;]*m/g, '').length;
1073
+ const pad = (text: string) => {
1074
+ const visible = visibleLength(text);
1075
+ return text + ' '.repeat(Math.max(0, w - visible - 3));
1076
+ };
1077
+
1078
+ const statusLine = `Status: ${statusColor}${statusText}\x1b[0m`;
1079
+
1080
+ const lines = [
1081
+ '+' + '-'.repeat(w - 2) + '+',
1082
+ '| ' + pad('Context Usage Statistics') + '|',
1083
+ '+' + '-'.repeat(w - 2) + '+',
1084
+ '| ' + pad(`Active Messages ${String(stats.activeMessages).padStart(3)}`) + '|',
1085
+ '| ' + pad(`Total Messages ${String(stats.totalMessages).padStart(3)}`) + '|',
1086
+ '| ' + pad(`Summaries ${String(stats.summaryCount).padStart(3)}`) + '|',
1087
+ '| ' + pad('') + '|',
1088
+ '| ' + pad(`Usage [${progressBar}] ${activeRatio.padStart(3)}%`) + '|',
1089
+ '| ' + pad('') + '|',
1090
+ '| ' + pad(statusLine) + '|',
1091
+ '+' + '-'.repeat(w - 2) + '+',
1092
+ ];
1093
+
1094
+ addHistory({
1095
+ type: 'info',
1096
+ content: '\n' + lines.join('\n'),
1097
+ });
1098
+ return true;
1099
+ }
1100
+
714
1101
  default:
715
1102
  return false;
716
1103
  }
@@ -718,6 +1105,8 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
718
1105
 
719
1106
  // Interrupt ref for ESC handling
720
1107
  const interruptFlagRef = useRef(false);
1108
+ // AbortController for cancellation support
1109
+ const abortControllerRef = useRef<AbortController | null>(null);
721
1110
 
722
1111
  // Run agent
723
1112
  const runAgent = async (prompt: string) => {
@@ -725,25 +1114,36 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
725
1114
  setIsThinking(true);
726
1115
  setStreamingText('');
727
1116
  streamingTextRef.current = '';
1117
+ // Clear streaming buffer and any pending flush timers
1118
+ streamBufferRef.current = '';
1119
+ if (flushTimerRef.current) {
1120
+ clearTimeout(flushTimerRef.current);
1121
+ flushTimerRef.current = null;
1122
+ }
728
1123
  interruptFlagRef.current = false;
1124
+
1125
+ // Create AbortController for this run
1126
+ const abortController = new AbortController();
1127
+ abortControllerRef.current = abortController;
1128
+
729
1129
  const startTime = Date.now();
730
1130
  setProcessingStartTime(startTime);
731
1131
  setTokenCount(0);
732
1132
 
733
1133
  try {
734
- for await (const event of agent.run(prompt)) {
1134
+ for await (const event of agent.run(prompt, abortController.signal)) {
735
1135
  // Check for interrupt
736
- if (interruptFlagRef.current) {
1136
+ if (interruptFlagRef.current || abortController.signal.aborted) {
737
1137
  break;
738
1138
  }
739
1139
 
740
1140
  switch (event.type) {
741
1141
  case 'text':
742
1142
  setIsThinking(false);
743
- streamingTextRef.current += event.text;
744
- setStreamingText(streamingTextRef.current);
745
- // Estimate token count (roughly 4 chars per token)
746
- setTokenCount((prev) => prev + Math.max(1, Math.ceil(event.text.length / 4)));
1143
+ // Use throttled streaming text update for better performance
1144
+ addStreamingText(event.text);
1145
+ // Estimate token count with language-aware estimation
1146
+ setTokenCount((prev) => prev + estimateTokenDelta(event.text));
747
1147
  break;
748
1148
 
749
1149
  case 'tool_start':
@@ -796,17 +1196,40 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
796
1196
  setIsThinking(true);
797
1197
  break;
798
1198
 
1199
+ case 'reasoning_delta':
1200
+ // Display reasoning content from o1/o3/Gemini 3+ models
1201
+ setIsThinking(false);
1202
+ addHistory({
1203
+ type: 'info',
1204
+ content: `💭 Reasoning: ${event.text}`,
1205
+ });
1206
+ break;
1207
+
1208
+ case 'tool_input_delta':
1209
+ // Progressive display of tool input JSON (optional enhancement)
1210
+ // For now, we just accumulate and display when complete
1211
+ // Could be enhanced to show partial JSON in real-time
1212
+ break;
1213
+
799
1214
  case 'error':
800
1215
  setIsThinking(false);
801
1216
  addHistory({ type: 'info', content: `Error: ${event.error.message}` });
802
1217
  break;
803
1218
 
804
1219
  case 'done':
1220
+ // Flush any remaining buffered text immediately
1221
+ flushStreamBuffer();
1222
+
805
1223
  if (streamingTextRef.current) {
806
1224
  addHistory({ type: 'assistant', content: streamingTextRef.current });
807
1225
  streamingTextRef.current = '';
1226
+ streamBufferRef.current = '';
808
1227
  setStreamingText('');
809
1228
  }
1229
+ // Use real token count from usage if available (overrides estimate)
1230
+ if (event.usage) {
1231
+ setTokenCount(event.usage.outputTokens);
1232
+ }
810
1233
  // Add completion message with duration and cost info
811
1234
  const durationMs = Date.now() - startTime;
812
1235
  addHistory({
@@ -823,10 +1246,27 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
823
1246
  type: 'info',
824
1247
  content: `Error: ${error instanceof Error ? error.message : String(error)}`,
825
1248
  });
1249
+ } finally {
1250
+ // Clean up AbortController
1251
+ abortControllerRef.current = null;
826
1252
  }
827
1253
 
828
1254
  setIsProcessing(false);
829
1255
  setIsThinking(false);
1256
+
1257
+ // Process next message in queue if any
1258
+ setMessageQueue((queue) => {
1259
+ if (queue.length > 0) {
1260
+ const [nextMessage, ...rest] = queue;
1261
+ // Schedule next message processing
1262
+ setTimeout(() => {
1263
+ addHistory({ type: 'user', content: nextMessage });
1264
+ runAgent(nextMessage);
1265
+ }, 0);
1266
+ return rest;
1267
+ }
1268
+ return queue;
1269
+ });
830
1270
  };
831
1271
 
832
1272
  // Handle submit
@@ -834,6 +1274,12 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
834
1274
  const trimmed = text.trim();
835
1275
  if (!trimmed) return;
836
1276
 
1277
+ // Add to input history
1278
+ if (historyManagerRef.current) {
1279
+ historyManagerRef.current.add(trimmed);
1280
+ historyManagerRef.current.reset(); // Reset navigation state
1281
+ }
1282
+
837
1283
  // Auto-complete command on Enter if no exact match
838
1284
  if (trimmed.startsWith('/') && cmdSuggestions.length > 0) {
839
1285
  const exactMatch = cmdSuggestions.find(
@@ -905,28 +1351,43 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
905
1351
  return;
906
1352
  }
907
1353
 
1354
+ // Queue message if already processing
1355
+ if (isProcessing) {
1356
+ setMessageQueue((queue) => [...queue, trimmed]);
1357
+ // Don't add history item - we'll show queue count in the UI
1358
+ return;
1359
+ }
1360
+
908
1361
  addHistory({ type: 'user', content: trimmed });
909
1362
  await runAgent(trimmed);
910
1363
  };
911
1364
 
912
1365
  // Keyboard shortcuts
913
1366
  useInput((inputChar, key) => {
914
- if (key.ctrl && inputChar === 'c') {
915
- agent.saveSession().then(() => exit());
916
- }
917
-
918
- // ESC to interrupt processing
919
- if (key.escape && isProcessing) {
920
- interruptFlagRef.current = true;
921
- setIsProcessing(false);
922
- setStreamingText('');
923
- streamingTextRef.current = '';
924
- // Clear pending tool (stop spinner)
925
- pendingToolRef.current = null;
926
- setPendingTool(null);
927
- // Clean up incomplete tool_use messages to prevent API errors
928
- agent.cleanupIncompleteMessages();
929
- addHistory({ type: 'info', content: 'Interrupted' });
1367
+ // ESC to interrupt processing or cancel history navigation
1368
+ if (key.escape) {
1369
+ if (isProcessing) {
1370
+ // Abort the operation
1371
+ if (abortControllerRef.current) {
1372
+ abortControllerRef.current.abort();
1373
+ }
1374
+ interruptFlagRef.current = true;
1375
+ setIsProcessing(false);
1376
+ setStreamingText('');
1377
+ streamingTextRef.current = '';
1378
+ streamBufferRef.current = '';
1379
+ // Clear pending tool (stop spinner)
1380
+ pendingToolRef.current = null;
1381
+ setPendingTool(null);
1382
+ // Clean up incomplete tool_use messages to prevent API errors
1383
+ agent.cleanupIncompleteMessages();
1384
+ addHistory({ type: 'info', content: 'Interrupted' });
1385
+ } else if (historyManagerRef.current?.isNavigating()) {
1386
+ // Cancel history navigation - restore original input
1387
+ historyManagerRef.current.reset();
1388
+ setInput(historyTempInput);
1389
+ setHistoryTempInput('');
1390
+ }
930
1391
  }
931
1392
 
932
1393
  // Shift+Tab to cycle modes: normal → plan → accept → normal
@@ -967,19 +1428,58 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
967
1428
  }
968
1429
  }
969
1430
  }
1431
+ // Input history navigation (when NOT showing command suggestions)
1432
+ else if (!isProcessing && !confirmState && !questionState && !planApprovalState && historyManagerRef.current) {
1433
+ if (key.upArrow) {
1434
+ // Save current input on first navigation
1435
+ if (!historyManagerRef.current.isNavigating()) {
1436
+ setHistoryTempInput(input);
1437
+ }
1438
+
1439
+ const prevEntry = historyManagerRef.current.previous();
1440
+ if (prevEntry !== null) {
1441
+ setInput(prevEntry);
1442
+ setInputKey((k) => k + 1); // Force cursor to end
1443
+ }
1444
+ } else if (key.downArrow && historyManagerRef.current.isNavigating()) {
1445
+ const nextEntry = historyManagerRef.current.next();
1446
+ if (nextEntry === null) {
1447
+ // Reached end - restore original input
1448
+ setInput(historyTempInput);
1449
+ setHistoryTempInput('');
1450
+ } else {
1451
+ setInput(nextEntry);
1452
+ }
1453
+ setInputKey((k) => k + 1); // Force cursor to end
1454
+ }
1455
+ }
970
1456
  });
971
1457
 
972
1458
  // Render history item
973
1459
  const renderHistoryItem = (item: HistoryItem) => {
974
1460
  switch (item.type) {
975
- case 'header':
1461
+ case 'header': {
1462
+ // Calculate context stats for header
1463
+ const sessionMgr = agent.getSessionManager();
1464
+ const compressionStats = sessionMgr.getCompressionStats();
1465
+ const tokenUsage = sessionMgr.getTokenUsage();
1466
+ const modelInfo = agent.getModelInfo();
1467
+
1468
+ const contextStats = compressionStats && modelInfo && compressionStats.activeMessages > 0 ? {
1469
+ activeMessages: compressionStats.activeMessages,
1470
+ totalMessages: compressionStats.totalMessages,
1471
+ usagePercent: (tokenUsage.total / modelInfo.contextWindow) * 100,
1472
+ } : undefined;
1473
+
976
1474
  return (
977
1475
  <Header
978
1476
  provider={(item.meta?.provider as string) || ''}
979
1477
  model={(item.meta?.model as string) || ''}
980
1478
  cwd={(item.meta?.cwd as string) || ''}
1479
+ contextStats={contextStats}
981
1480
  />
982
1481
  );
1482
+ }
983
1483
  case 'welcome':
984
1484
  return <WelcomeMessage model={(item.meta?.model as string) || item.content} />;
985
1485
  case 'user':
@@ -1025,6 +1525,14 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
1025
1525
  if (item.content === '__MEMORY__' && item.meta?.files) {
1026
1526
  return <MemoryFilesDisplay files={item.meta.files as MemoryFileInfo[]} />;
1027
1527
  }
1528
+ // Check if content is a formatted box (starts with box border)
1529
+ if (item.content.trim().startsWith('+---')) {
1530
+ return (
1531
+ <Box marginTop={1}>
1532
+ <Text color={colors.textSecondary}>{item.content}</Text>
1533
+ </Box>
1534
+ );
1535
+ }
1028
1536
  return <InfoMessage text={item.content} />;
1029
1537
  case 'completion':
1030
1538
  return (
@@ -1113,6 +1621,17 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
1113
1621
 
1114
1622
  {!confirmState && !questionState && !showModelSelector && !showProviderManager && (
1115
1623
  <Box flexDirection="column" marginTop={2}>
1624
+ {/* Queue display above input */}
1625
+ {messageQueue.length > 0 && (
1626
+ <Box flexDirection="column" marginBottom={1}>
1627
+ {messageQueue.map((msg, i) => (
1628
+ <Text key={i} color={colors.textMuted}>
1629
+ ⏳ <Text color={colors.text}>{msg.length > 60 ? msg.slice(0, 60) + '...' : msg}</Text>
1630
+ </Text>
1631
+ ))}
1632
+ </Box>
1633
+ )}
1634
+
1116
1635
  <PromptInput
1117
1636
  key={inputKey}
1118
1637
  value={input}