gencode-ai 0.1.3 → 0.2.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 (151) hide show
  1. package/README.md +2 -1
  2. package/dist/agent/agent.d.ts +35 -0
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +93 -3
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +6 -0
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/checkpointing/checkpoint-manager.d.ts +87 -0
  9. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -0
  10. package/dist/checkpointing/checkpoint-manager.js +281 -0
  11. package/dist/checkpointing/checkpoint-manager.js.map +1 -0
  12. package/dist/checkpointing/index.d.ts +29 -0
  13. package/dist/checkpointing/index.d.ts.map +1 -0
  14. package/dist/checkpointing/index.js +29 -0
  15. package/dist/checkpointing/index.js.map +1 -0
  16. package/dist/checkpointing/types.d.ts +98 -0
  17. package/dist/checkpointing/types.d.ts.map +1 -0
  18. package/dist/checkpointing/types.js +7 -0
  19. package/dist/checkpointing/types.js.map +1 -0
  20. package/dist/cli/components/App.d.ts.map +1 -1
  21. package/dist/cli/components/App.js +157 -6
  22. package/dist/cli/components/App.js.map +1 -1
  23. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  24. package/dist/cli/components/CommandSuggestions.js +5 -0
  25. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  26. package/dist/cli/components/Messages.d.ts +7 -1
  27. package/dist/cli/components/Messages.d.ts.map +1 -1
  28. package/dist/cli/components/Messages.js +11 -2
  29. package/dist/cli/components/Messages.js.map +1 -1
  30. package/dist/cli/components/ModeIndicator.d.ts +42 -0
  31. package/dist/cli/components/ModeIndicator.d.ts.map +1 -0
  32. package/dist/cli/components/ModeIndicator.js +52 -0
  33. package/dist/cli/components/ModeIndicator.js.map +1 -0
  34. package/dist/cli/components/PlanApproval.d.ts +36 -0
  35. package/dist/cli/components/PlanApproval.d.ts.map +1 -0
  36. package/dist/cli/components/PlanApproval.js +154 -0
  37. package/dist/cli/components/PlanApproval.js.map +1 -0
  38. package/dist/cli/components/theme.d.ts +2 -0
  39. package/dist/cli/components/theme.d.ts.map +1 -1
  40. package/dist/cli/components/theme.js +3 -0
  41. package/dist/cli/components/theme.js.map +1 -1
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/planning/index.d.ts +13 -0
  47. package/dist/planning/index.d.ts.map +1 -0
  48. package/dist/planning/index.js +15 -0
  49. package/dist/planning/index.js.map +1 -0
  50. package/dist/planning/plan-file.d.ts +59 -0
  51. package/dist/planning/plan-file.d.ts.map +1 -0
  52. package/dist/planning/plan-file.js +278 -0
  53. package/dist/planning/plan-file.js.map +1 -0
  54. package/dist/planning/state.d.ts +127 -0
  55. package/dist/planning/state.d.ts.map +1 -0
  56. package/dist/planning/state.js +261 -0
  57. package/dist/planning/state.js.map +1 -0
  58. package/dist/planning/tools/enter-plan-mode.d.ts +25 -0
  59. package/dist/planning/tools/enter-plan-mode.d.ts.map +1 -0
  60. package/dist/planning/tools/enter-plan-mode.js +98 -0
  61. package/dist/planning/tools/enter-plan-mode.js.map +1 -0
  62. package/dist/planning/tools/exit-plan-mode.d.ts +24 -0
  63. package/dist/planning/tools/exit-plan-mode.d.ts.map +1 -0
  64. package/dist/planning/tools/exit-plan-mode.js +149 -0
  65. package/dist/planning/tools/exit-plan-mode.js.map +1 -0
  66. package/dist/planning/types.d.ts +100 -0
  67. package/dist/planning/types.d.ts.map +1 -0
  68. package/dist/planning/types.js +28 -0
  69. package/dist/planning/types.js.map +1 -0
  70. package/dist/pricing/calculator.d.ts +21 -0
  71. package/dist/pricing/calculator.d.ts.map +1 -0
  72. package/dist/pricing/calculator.js +59 -0
  73. package/dist/pricing/calculator.js.map +1 -0
  74. package/dist/pricing/index.d.ts +7 -0
  75. package/dist/pricing/index.d.ts.map +1 -0
  76. package/dist/pricing/index.js +7 -0
  77. package/dist/pricing/index.js.map +1 -0
  78. package/dist/pricing/models.d.ts +20 -0
  79. package/dist/pricing/models.d.ts.map +1 -0
  80. package/dist/pricing/models.js +322 -0
  81. package/dist/pricing/models.js.map +1 -0
  82. package/dist/pricing/types.d.ts +30 -0
  83. package/dist/pricing/types.d.ts.map +1 -0
  84. package/dist/pricing/types.js +5 -0
  85. package/dist/pricing/types.js.map +1 -0
  86. package/dist/providers/anthropic.d.ts.map +1 -1
  87. package/dist/providers/anthropic.js +17 -10
  88. package/dist/providers/anthropic.js.map +1 -1
  89. package/dist/providers/gemini.d.ts.map +1 -1
  90. package/dist/providers/gemini.js +21 -14
  91. package/dist/providers/gemini.js.map +1 -1
  92. package/dist/providers/openai.d.ts.map +1 -1
  93. package/dist/providers/openai.js +12 -8
  94. package/dist/providers/openai.js.map +1 -1
  95. package/dist/providers/types.d.ts +2 -0
  96. package/dist/providers/types.d.ts.map +1 -1
  97. package/dist/providers/vertex-ai.d.ts.map +1 -1
  98. package/dist/providers/vertex-ai.js +17 -10
  99. package/dist/providers/vertex-ai.js.map +1 -1
  100. package/dist/session/manager.d.ts +4 -0
  101. package/dist/session/manager.d.ts.map +1 -1
  102. package/dist/session/manager.js +8 -0
  103. package/dist/session/manager.js.map +1 -1
  104. package/dist/tools/index.d.ts +7 -1
  105. package/dist/tools/index.d.ts.map +1 -1
  106. package/dist/tools/index.js +7 -0
  107. package/dist/tools/index.js.map +1 -1
  108. package/dist/tools/registry.d.ts +13 -0
  109. package/dist/tools/registry.d.ts.map +1 -1
  110. package/dist/tools/registry.js +79 -2
  111. package/dist/tools/registry.js.map +1 -1
  112. package/docs/cost-tracking-comparison.md +904 -0
  113. package/docs/operating-modes.md +96 -0
  114. package/docs/proposals/0025-cost-tracking.md +60 -2
  115. package/docs/proposals/README.md +1 -1
  116. package/examples/test-checkpointing.ts +121 -0
  117. package/examples/test-cost-tracking.ts +77 -0
  118. package/examples/test-interrupt-cleanup.ts +94 -0
  119. package/package.json +1 -1
  120. package/src/agent/agent.ts +110 -3
  121. package/src/agent/types.ts +6 -0
  122. package/src/checkpointing/checkpoint-manager.ts +327 -0
  123. package/src/checkpointing/index.ts +45 -0
  124. package/src/checkpointing/types.ts +104 -0
  125. package/src/cli/components/App.tsx +204 -5
  126. package/src/cli/components/CommandSuggestions.tsx +5 -0
  127. package/src/cli/components/Messages.tsx +23 -4
  128. package/src/cli/components/ModeIndicator.tsx +174 -0
  129. package/src/cli/components/PlanApproval.tsx +327 -0
  130. package/src/cli/components/theme.ts +3 -0
  131. package/src/index.ts +15 -0
  132. package/src/planning/index.ts +53 -0
  133. package/src/planning/plan-file.ts +326 -0
  134. package/src/planning/state.ts +305 -0
  135. package/src/planning/tools/enter-plan-mode.ts +111 -0
  136. package/src/planning/tools/exit-plan-mode.ts +170 -0
  137. package/src/planning/types.ts +150 -0
  138. package/src/pricing/calculator.ts +71 -0
  139. package/src/pricing/index.ts +7 -0
  140. package/src/pricing/models.ts +334 -0
  141. package/src/pricing/types.ts +32 -0
  142. package/src/providers/anthropic.ts +21 -10
  143. package/src/providers/gemini.ts +25 -14
  144. package/src/providers/openai.ts +17 -8
  145. package/src/providers/types.ts +3 -0
  146. package/src/providers/vertex-ai.ts +21 -10
  147. package/src/session/manager.ts +9 -0
  148. package/src/tools/index.ts +8 -0
  149. package/src/tools/registry.ts +95 -2
  150. package/.gencode/settings.local.json +0 -7
  151. package/CLAUDE.md +0 -86
@@ -6,6 +6,8 @@ import { useState, useEffect, useCallback, useRef } from 'react';
6
6
  import { Box, Text, useApp, useInput, Static } from 'ink';
7
7
  import { Agent } from '../../agent/index.js';
8
8
  import type { AgentConfig } from '../../agent/types.js';
9
+ import { formatTokens, formatCost } from '../../pricing/calculator.js';
10
+ import type { CostEstimate } from '../../pricing/types.js';
9
11
  import {
10
12
  UserMessage,
11
13
  AssistantMessage,
@@ -35,6 +37,11 @@ import type { Question, QuestionAnswer } from '../../tools/types.js';
35
37
  import type { ProviderName } from '../../providers/index.js';
36
38
  import type { ApprovalAction, ApprovalSuggestion } from '../../permissions/types.js';
37
39
  import { gatherContextFiles, buildInitPrompt, getContextSummary } from '../../memory/index.js';
40
+ // ModeIndicator kept for potential future use
41
+ import { PlanApproval } from './PlanApproval.js';
42
+ import type { ModeType, PlanApprovalOption, AllowedPrompt } from '../../planning/types.js';
43
+ // Planning utilities kept for potential future use
44
+ import { getCheckpointManager } from '../../checkpointing/index.js';
38
45
 
39
46
  // Types
40
47
  interface HistoryItem {
@@ -56,6 +63,14 @@ interface QuestionState {
56
63
  resolve: (answers: QuestionAnswer[]) => void;
57
64
  }
58
65
 
66
+ interface PlanApprovalState {
67
+ planSummary: string;
68
+ requestedPermissions: AllowedPrompt[];
69
+ filesToChange: Array<{ path: string; action: 'create' | 'modify' | 'delete' }>;
70
+ planFilePath: string;
71
+ resolve: (option: PlanApprovalOption, customInput?: string) => void;
72
+ }
73
+
59
74
  interface SettingsManager {
60
75
  save: (settings: { model?: string }) => Promise<void>;
61
76
  getCwd?: () => string;
@@ -108,6 +123,9 @@ const formatRelativeTime = (dateStr: string) => {
108
123
  // ============================================================================
109
124
  function HelpPanel() {
110
125
  const commands: [string, string][] = [
126
+ ['/plan [desc]', 'Enter plan mode'],
127
+ ['/normal', 'Exit to normal mode'],
128
+ ['/accept', 'Enter auto-accept mode'],
111
129
  ['/model [name]', 'Switch model'],
112
130
  ['/provider', 'Manage providers'],
113
131
  ['/sessions', 'List sessions'],
@@ -117,6 +135,8 @@ function HelpPanel() {
117
135
  ['/clear', 'Clear chat'],
118
136
  ['/init', 'Generate AGENT.md'],
119
137
  ['/memory', 'Show memory files'],
138
+ ['/changes', 'List file changes'],
139
+ ['/rewind [n|all]', 'Undo file changes'],
120
140
  ];
121
141
 
122
142
  return (
@@ -252,6 +272,16 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
252
272
  const pendingToolRef = useRef<{ name: string; input: Record<string, unknown> } | null>(null);
253
273
  const [todos, setTodos] = useState<ReturnType<typeof getTodos>>([]);
254
274
 
275
+ // Operating mode state (normal → plan → accept → normal)
276
+ const [currentMode, setCurrentMode] = useState<ModeType>('normal');
277
+ const currentModeRef = useRef<ModeType>('normal'); // Track mode for confirm callback
278
+ const [planApprovalState, setPlanApprovalState] = useState<PlanApprovalState | null>(null);
279
+
280
+ // Keep ref in sync with state
281
+ useEffect(() => {
282
+ currentModeRef.current = currentMode;
283
+ }, [currentMode]);
284
+
255
285
  // Check if showing command suggestions
256
286
  const showCmdSuggestions = input.startsWith('/') && !isProcessing;
257
287
  const cmdSuggestions = showCmdSuggestions ? getFilteredCommands(input) : [];
@@ -274,6 +304,11 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
274
304
 
275
305
  // Set enhanced confirm callback with approval options
276
306
  agent.setEnhancedConfirmCallback(async (tool, toolInput, suggestions) => {
307
+ // Auto-approve in accept mode
308
+ if (currentModeRef.current === 'accept') {
309
+ return 'allow_once';
310
+ }
311
+
277
312
  return new Promise<ApprovalAction>((resolve) => {
278
313
  setConfirmState({
279
314
  tool,
@@ -565,6 +600,110 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
565
600
  return true;
566
601
  }
567
602
 
603
+ case 'plan': {
604
+ // Enter plan mode
605
+ await agent.enterPlanMode(arg);
606
+ setCurrentMode('plan');
607
+ return true;
608
+ }
609
+
610
+ case 'normal': {
611
+ // Exit to normal mode
612
+ if (agent.isPlanModeActive()) {
613
+ agent.exitPlanMode(false);
614
+ }
615
+ setCurrentMode('normal');
616
+ return true;
617
+ }
618
+
619
+ case 'accept': {
620
+ // Enter auto-accept mode
621
+ if (agent.isPlanModeActive()) {
622
+ agent.exitPlanMode(false);
623
+ }
624
+ setCurrentMode('accept');
625
+ return true;
626
+ }
627
+
628
+ case 'changes': {
629
+ // List file changes (checkpoints)
630
+ const checkpointManager = getCheckpointManager();
631
+ if (!checkpointManager.hasCheckpoints()) {
632
+ addHistory({ type: 'info', content: '\nNo file changes in this session' });
633
+ } else {
634
+ addHistory({ type: 'info', content: '\n' + checkpointManager.formatCheckpointList(false) });
635
+ }
636
+ return true;
637
+ }
638
+
639
+ case 'rewind': {
640
+ // Rewind file changes
641
+ const checkpointManager = getCheckpointManager();
642
+
643
+ if (!checkpointManager.hasCheckpoints()) {
644
+ addHistory({ type: 'info', content: 'No file changes to rewind' });
645
+ return true;
646
+ }
647
+
648
+ if (arg === 'all') {
649
+ // Rewind all changes
650
+ const result = await checkpointManager.rewind({ all: true });
651
+
652
+ // Build output message showing both successes and failures
653
+ const messages: string[] = ['']; // Start with empty line for spacing
654
+
655
+ if (result.revertedFiles.length > 0) {
656
+ const files = result.revertedFiles.map((f) => {
657
+ const fileName = f.path.split('/').pop() || f.path;
658
+ return ` • ${fileName} (${f.action})`;
659
+ }).join('\n');
660
+ messages.push(`Reverted ${result.revertedFiles.length} file(s):\n${files}`);
661
+ }
662
+
663
+ if (result.errors.length > 0) {
664
+ const errors = result.errors.map((e) => {
665
+ const fileName = e.path.split('/').pop() || e.path;
666
+ return ` • ${fileName}: ${e.error}`;
667
+ }).join('\n');
668
+ messages.push(`\nFailed to revert ${result.errors.length} file(s):\n${errors}`);
669
+ }
670
+
671
+ if (messages.length > 1) {
672
+ addHistory({ type: 'info', content: messages.join('\n') });
673
+ } else {
674
+ addHistory({ type: 'info', content: '\nNo changes to rewind' });
675
+ }
676
+ } else if (arg) {
677
+ // Rewind specific checkpoint by index
678
+ const index = parseInt(arg, 10);
679
+ if (!isNaN(index) && index >= 1) {
680
+ const checkpoints = checkpointManager.getCheckpoints();
681
+ if (index <= checkpoints.length) {
682
+ const checkpoint = checkpoints[index - 1];
683
+ const result = await checkpointManager.rewind({ checkpointId: checkpoint.id });
684
+ if (result.success && result.revertedFiles.length > 0) {
685
+ const f = result.revertedFiles[0];
686
+ const fileName = f.path.split('/').pop() || f.path;
687
+ addHistory({ type: 'info', content: `\nReverted: ${fileName} (${f.action})` });
688
+ } else if (result.errors.length > 0) {
689
+ const fileName = result.errors[0].path.split('/').pop() || result.errors[0].path;
690
+ addHistory({ type: 'info', content: `\nFailed: ${fileName} - ${result.errors[0].error}` });
691
+ } else {
692
+ addHistory({ type: 'info', content: '\nFailed to rewind change' });
693
+ }
694
+ } else {
695
+ addHistory({ type: 'info', content: '\nInvalid index: ${index}' });
696
+ }
697
+ } else {
698
+ addHistory({ type: 'info', content: '\nUsage: /rewind [n|all]' });
699
+ }
700
+ } else {
701
+ // Show changes and usage in one message
702
+ addHistory({ type: 'info', content: '\n' + checkpointManager.formatCheckpointList(true) });
703
+ }
704
+ return true;
705
+ }
706
+
568
707
  default:
569
708
  return false;
570
709
  }
@@ -661,9 +800,13 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
661
800
  streamingTextRef.current = '';
662
801
  setStreamingText('');
663
802
  }
664
- // Add completion message with duration
803
+ // Add completion message with duration and cost info
665
804
  const durationMs = Date.now() - startTime;
666
- addHistory({ type: 'completion', content: '', meta: { durationMs } });
805
+ addHistory({
806
+ type: 'completion',
807
+ content: '',
808
+ meta: { durationMs, usage: event.usage, cost: event.cost },
809
+ });
667
810
  setProcessingStartTime(undefined);
668
811
  break;
669
812
  }
@@ -774,9 +917,33 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
774
917
  // Clear pending tool (stop spinner)
775
918
  pendingToolRef.current = null;
776
919
  setPendingTool(null);
920
+ // Clean up incomplete tool_use messages to prevent API errors
921
+ agent.cleanupIncompleteMessages();
777
922
  addHistory({ type: 'info', content: 'Interrupted' });
778
923
  }
779
924
 
925
+ // Shift+Tab to cycle modes: normal → plan → accept → normal
926
+ if (key.shift && key.tab && !isProcessing && !confirmState && !questionState && !planApprovalState) {
927
+ const cycleMode = async () => {
928
+ const nextMode: Record<ModeType, ModeType> = {
929
+ normal: 'plan',
930
+ plan: 'accept',
931
+ accept: 'normal',
932
+ };
933
+ const newMode = nextMode[currentMode];
934
+
935
+ // Handle plan mode transitions
936
+ if (newMode === 'plan') {
937
+ await agent.enterPlanMode();
938
+ } else if (currentMode === 'plan') {
939
+ agent.exitPlanMode(false);
940
+ }
941
+
942
+ setCurrentMode(newMode);
943
+ };
944
+ cycleMode();
945
+ }
946
+
780
947
  // Command suggestion navigation
781
948
  if (showCmdSuggestions && cmdSuggestions.length > 0) {
782
949
  if (key.upArrow) {
@@ -853,7 +1020,13 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
853
1020
  }
854
1021
  return <InfoMessage text={item.content} />;
855
1022
  case 'completion':
856
- return <CompletionMessage durationMs={(item.meta?.durationMs as number) || 0} />;
1023
+ return (
1024
+ <CompletionMessage
1025
+ durationMs={(item.meta?.durationMs as number) || 0}
1026
+ usage={item.meta?.usage as { inputTokens: number; outputTokens: number } | undefined}
1027
+ cost={item.meta?.cost as CostEstimate | undefined}
1028
+ />
1029
+ );
857
1030
  case 'todos':
858
1031
  return <TodoList todos={item.meta?.todos as ReturnType<typeof getTodos>} />;
859
1032
  default:
@@ -862,7 +1035,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
862
1035
  };
863
1036
 
864
1037
  return (
865
- <Box flexDirection="column">
1038
+ <Box flexDirection="column" paddingBottom={2}>
866
1039
  <Static items={history}>
867
1040
  {(item) => <Box key={item.id}>{renderHistoryItem(item)}</Box>}
868
1041
  </Static>
@@ -889,6 +1062,22 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
889
1062
  />
890
1063
  )}
891
1064
 
1065
+ {/* Plan approval UI */}
1066
+ {planApprovalState && (
1067
+ <Box marginTop={1}>
1068
+ <PlanApproval
1069
+ planSummary={planApprovalState.planSummary}
1070
+ requestedPermissions={planApprovalState.requestedPermissions}
1071
+ filesToChange={planApprovalState.filesToChange}
1072
+ planFilePath={planApprovalState.planFilePath}
1073
+ onDecision={(option, customInput) => {
1074
+ planApprovalState.resolve(option, customInput);
1075
+ setPlanApprovalState(null);
1076
+ }}
1077
+ />
1078
+ </Box>
1079
+ )}
1080
+
892
1081
  {showModelSelector && (
893
1082
  <Box marginTop={1}>
894
1083
  <ModelSelector
@@ -914,7 +1103,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
914
1103
  )}
915
1104
 
916
1105
  {!confirmState && !questionState && !showModelSelector && !showProviderManager && (
917
- <Box flexDirection="column" marginTop={1}>
1106
+ <Box flexDirection="column" marginTop={2}>
918
1107
  <PromptInput
919
1108
  key={inputKey}
920
1109
  value={input}
@@ -924,6 +1113,16 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
924
1113
  {showCmdSuggestions && cmdSuggestions.length > 0 && (
925
1114
  <CommandSuggestions input={input} selectedIndex={cmdSuggestionIndex} />
926
1115
  )}
1116
+ {currentMode === 'plan' && !isProcessing && (
1117
+ <Text color={colors.warning} dimColor>
1118
+ {icons.modePlan} plan mode on (shift+tab to cycle)
1119
+ </Text>
1120
+ )}
1121
+ {currentMode === 'accept' && !isProcessing && (
1122
+ <Text color={colors.success} dimColor>
1123
+ {icons.modeAccept} accept edits on (shift+tab to cycle)
1124
+ </Text>
1125
+ )}
927
1126
  </Box>
928
1127
  )}
929
1128
 
@@ -7,6 +7,9 @@ interface Command {
7
7
  }
8
8
 
9
9
  export const COMMANDS: Command[] = [
10
+ { name: '/plan', description: 'Enter plan mode (Shift+Tab to cycle)' },
11
+ { name: '/normal', description: 'Exit to normal mode' },
12
+ { name: '/accept', description: 'Enter auto-accept mode' },
10
13
  { name: '/model', description: 'Switch model' },
11
14
  { name: '/provider', description: 'Manage providers' },
12
15
  { name: '/permissions', description: 'View permission rules' },
@@ -20,6 +23,8 @@ export const COMMANDS: Command[] = [
20
23
  { name: '/help', description: 'Show help' },
21
24
  { name: '/init', description: 'Generate AGENT.md' },
22
25
  { name: '/memory', description: 'Show memory files' },
26
+ { name: '/changes', description: 'List file changes' },
27
+ { name: '/rewind', description: 'Undo file changes' },
23
28
  ];
24
29
 
25
30
  interface CommandSuggestionsProps {
@@ -3,6 +3,8 @@ import { Box, Text } from 'ink';
3
3
  import InkSpinner from 'ink-spinner';
4
4
  import { colors, icons } from './theme.js';
5
5
  import { renderMarkdown } from './markdown.js';
6
+ import { formatTokens, formatCost } from '../../pricing/calculator.js';
7
+ import type { CostEstimate } from '../../pricing/types.js';
6
8
 
7
9
  // Truncate string with ellipsis
8
10
  const truncate = (str: string, maxLen: number) =>
@@ -346,16 +348,33 @@ function formatDuration(ms: number): string {
346
348
 
347
349
  interface CompletionMessageProps {
348
350
  durationMs: number;
351
+ usage?: {
352
+ inputTokens: number;
353
+ outputTokens: number;
354
+ };
355
+ cost?: CostEstimate;
349
356
  }
350
357
 
351
- export function CompletionMessage({ durationMs }: CompletionMessageProps) {
358
+ export function CompletionMessage({ durationMs, usage, cost }: CompletionMessageProps) {
352
359
  // Pick a random verb (stable per render via useMemo would be better, but keep simple)
353
360
  const verb = COMPLETION_VERBS[Math.floor(Math.random() * COMPLETION_VERBS.length)];
361
+
362
+ // Build the message parts
363
+ const parts = [`✻ ${verb} for ${formatDuration(durationMs)}`];
364
+
365
+ if (usage) {
366
+ parts.push(
367
+ `Tokens: ${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out`
368
+ );
369
+ }
370
+
371
+ if (cost) {
372
+ parts.push(`(~${formatCost(cost.totalCost)})`);
373
+ }
374
+
354
375
  return (
355
376
  <Box marginTop={1}>
356
- <Text color={colors.textMuted}>
357
- ✻ {verb} for {formatDuration(durationMs)}
358
- </Text>
377
+ <Text color={colors.textMuted}>{parts.join(' • ')}</Text>
359
378
  </Box>
360
379
  );
361
380
  }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Mode Indicator Component - Shows current mode (NORMAL/PLAN/ACCEPT)
3
+ *
4
+ * Design:
5
+ * ╭────────────────────────────────────╮
6
+ * │ ◉ NORMAL ○ PLAN ○ ACCEPT │
7
+ * ╰────────────────────────────────────╯
8
+ *
9
+ * Or with Shift+Tab toggle hint
10
+ */
11
+
12
+ import { Box, Text } from 'ink';
13
+ import { colors, icons } from './theme.js';
14
+ import type { ModeType } from '../../planning/types.js';
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ interface ModeIndicatorProps {
21
+ /** Current mode */
22
+ mode: ModeType;
23
+ /** Show toggle hint */
24
+ showHint?: boolean;
25
+ /** Compact mode (inline) */
26
+ compact?: boolean;
27
+ }
28
+
29
+ // ============================================================================
30
+ // Mode Indicator Component
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Mode Indicator - Shows NORMAL/PLAN/ACCEPT mode toggle
35
+ */
36
+ export function ModeIndicator({ mode, showHint = false, compact = false }: ModeIndicatorProps) {
37
+ const isNormal = mode === 'normal';
38
+ const isPlan = mode === 'plan';
39
+ const isAccept = mode === 'accept';
40
+
41
+ if (compact) {
42
+ // Compact inline indicator
43
+ const modeLabel = isPlan ? 'PLAN' : isAccept ? 'ACCEPT' : '';
44
+ const modeColor = isPlan ? colors.warning : isAccept ? colors.success : colors.textMuted;
45
+
46
+ if (!modeLabel) return null;
47
+
48
+ return (
49
+ <Box>
50
+ <Text color={modeColor}>
51
+ {isPlan ? icons.modePlan : icons.modeAccept}
52
+ </Text>
53
+ <Text color={modeColor}> {modeLabel}</Text>
54
+ </Box>
55
+ );
56
+ }
57
+
58
+ return (
59
+ <Box flexDirection="column">
60
+ <Box borderStyle="round" borderColor={isPlan ? colors.warning : isAccept ? colors.success : colors.textMuted} paddingX={1}>
61
+ {/* NORMAL option */}
62
+ <Text color={isNormal ? colors.primary : colors.textMuted}>
63
+ {isNormal ? icons.radio : icons.radioEmpty}
64
+ </Text>
65
+ <Text color={isNormal ? colors.text : colors.textMuted} bold={isNormal}>
66
+ {' '}NORMAL
67
+ </Text>
68
+
69
+ <Text color={colors.textMuted}> </Text>
70
+
71
+ {/* PLAN option */}
72
+ <Text color={isPlan ? colors.warning : colors.textMuted}>
73
+ {isPlan ? icons.radio : icons.radioEmpty}
74
+ </Text>
75
+ <Text color={isPlan ? colors.text : colors.textMuted} bold={isPlan}>
76
+ {' '}PLAN
77
+ </Text>
78
+
79
+ <Text color={colors.textMuted}> </Text>
80
+
81
+ {/* ACCEPT option */}
82
+ <Text color={isAccept ? colors.success : colors.textMuted}>
83
+ {isAccept ? icons.radio : icons.radioEmpty}
84
+ </Text>
85
+ <Text color={isAccept ? colors.text : colors.textMuted} bold={isAccept}>
86
+ {' '}ACCEPT
87
+ </Text>
88
+ </Box>
89
+
90
+ {showHint && (
91
+ <Box marginTop={0}>
92
+ <Text color={colors.textMuted} dimColor> Shift+Tab to cycle modes</Text>
93
+ </Box>
94
+ )}
95
+ </Box>
96
+ );
97
+ }
98
+
99
+ // ============================================================================
100
+ // Mode Badge Component
101
+ // ============================================================================
102
+
103
+ interface ModeBadgeProps {
104
+ mode: ModeType;
105
+ }
106
+
107
+ /**
108
+ * Mode Badge - Compact badge for header display
109
+ */
110
+ export function ModeBadge({ mode }: ModeBadgeProps) {
111
+ const color = mode === 'plan' ? colors.warning : mode === 'accept' ? colors.success : colors.primary;
112
+ const label = mode === 'plan' ? 'PLAN' : mode === 'accept' ? 'ACCEPT' : 'NORMAL';
113
+
114
+ return (
115
+ <Text color={color} bold>
116
+ [{label}]
117
+ </Text>
118
+ );
119
+ }
120
+
121
+ // ============================================================================
122
+ // Plan Status Bar Component
123
+ // ============================================================================
124
+
125
+ interface PlanStatusBarProps {
126
+ /** Current planning phase */
127
+ phase: string;
128
+ /** Plan file path */
129
+ planFilePath?: string;
130
+ }
131
+
132
+ /**
133
+ * Plan Status Bar - Shows plan mode status
134
+ */
135
+ export function PlanStatusBar({ phase, planFilePath }: PlanStatusBarProps) {
136
+ // Shorten plan file path for display
137
+ const displayPath = planFilePath
138
+ ? planFilePath.replace(process.env.HOME || '', '~').split('/').slice(-2).join('/')
139
+ : '';
140
+
141
+ return (
142
+ <Box
143
+ borderStyle="round"
144
+ borderColor={colors.warning}
145
+ paddingX={1}
146
+ flexDirection="column"
147
+ >
148
+ {/* Status line */}
149
+ <Box>
150
+ <Text color={colors.warning}>PLAN MODE</Text>
151
+ <Text color={colors.textMuted}> │ </Text>
152
+ <Text color={colors.textSecondary}>Phase: </Text>
153
+ <Text color={colors.info}>{phase}</Text>
154
+ <Text color={colors.textMuted}> │ </Text>
155
+ <Text color={colors.textMuted}>Shift+Tab to switch</Text>
156
+ </Box>
157
+
158
+ {/* Tools info */}
159
+ <Box>
160
+ <Text color={colors.textMuted}>
161
+ Allowed: Read, Glob, Grep, WebFetch, WebSearch, TodoWrite
162
+ </Text>
163
+ </Box>
164
+
165
+ {/* Plan file */}
166
+ {displayPath && (
167
+ <Box>
168
+ <Text color={colors.textMuted}>Plan: </Text>
169
+ <Text color={colors.primary}>{displayPath}</Text>
170
+ </Box>
171
+ )}
172
+ </Box>
173
+ );
174
+ }