gencode-ai 0.1.2 → 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 (180) hide show
  1. package/README.md +15 -17
  2. package/dist/agent/agent.d.ts +43 -0
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +107 -4
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/index.d.ts +1 -0
  7. package/dist/agent/index.d.ts.map +1 -1
  8. package/dist/agent/types.d.ts +20 -1
  9. package/dist/agent/types.d.ts.map +1 -1
  10. package/dist/checkpointing/checkpoint-manager.d.ts +87 -0
  11. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -0
  12. package/dist/checkpointing/checkpoint-manager.js +281 -0
  13. package/dist/checkpointing/checkpoint-manager.js.map +1 -0
  14. package/dist/checkpointing/index.d.ts +29 -0
  15. package/dist/checkpointing/index.d.ts.map +1 -0
  16. package/dist/checkpointing/index.js +29 -0
  17. package/dist/checkpointing/index.js.map +1 -0
  18. package/dist/checkpointing/types.d.ts +98 -0
  19. package/dist/checkpointing/types.d.ts.map +1 -0
  20. package/dist/checkpointing/types.js +7 -0
  21. package/dist/checkpointing/types.js.map +1 -0
  22. package/dist/cli/components/App.d.ts.map +1 -1
  23. package/dist/cli/components/App.js +193 -7
  24. package/dist/cli/components/App.js.map +1 -1
  25. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  26. package/dist/cli/components/CommandSuggestions.js +5 -0
  27. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  28. package/dist/cli/components/Messages.d.ts +7 -1
  29. package/dist/cli/components/Messages.d.ts.map +1 -1
  30. package/dist/cli/components/Messages.js +28 -2
  31. package/dist/cli/components/Messages.js.map +1 -1
  32. package/dist/cli/components/ModeIndicator.d.ts +42 -0
  33. package/dist/cli/components/ModeIndicator.d.ts.map +1 -0
  34. package/dist/cli/components/ModeIndicator.js +52 -0
  35. package/dist/cli/components/ModeIndicator.js.map +1 -0
  36. package/dist/cli/components/PlanApproval.d.ts +36 -0
  37. package/dist/cli/components/PlanApproval.d.ts.map +1 -0
  38. package/dist/cli/components/PlanApproval.js +154 -0
  39. package/dist/cli/components/PlanApproval.js.map +1 -0
  40. package/dist/cli/components/QuestionPrompt.d.ts +23 -0
  41. package/dist/cli/components/QuestionPrompt.d.ts.map +1 -0
  42. package/dist/cli/components/QuestionPrompt.js +231 -0
  43. package/dist/cli/components/QuestionPrompt.js.map +1 -0
  44. package/dist/cli/components/index.d.ts +1 -0
  45. package/dist/cli/components/index.d.ts.map +1 -1
  46. package/dist/cli/components/index.js +1 -0
  47. package/dist/cli/components/index.js.map +1 -1
  48. package/dist/cli/components/theme.d.ts +9 -0
  49. package/dist/cli/components/theme.d.ts.map +1 -1
  50. package/dist/cli/components/theme.js +14 -1
  51. package/dist/cli/components/theme.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/permissions/types.d.ts.map +1 -1
  57. package/dist/permissions/types.js +2 -0
  58. package/dist/permissions/types.js.map +1 -1
  59. package/dist/planning/index.d.ts +13 -0
  60. package/dist/planning/index.d.ts.map +1 -0
  61. package/dist/planning/index.js +15 -0
  62. package/dist/planning/index.js.map +1 -0
  63. package/dist/planning/plan-file.d.ts +59 -0
  64. package/dist/planning/plan-file.d.ts.map +1 -0
  65. package/dist/planning/plan-file.js +278 -0
  66. package/dist/planning/plan-file.js.map +1 -0
  67. package/dist/planning/state.d.ts +127 -0
  68. package/dist/planning/state.d.ts.map +1 -0
  69. package/dist/planning/state.js +261 -0
  70. package/dist/planning/state.js.map +1 -0
  71. package/dist/planning/tools/enter-plan-mode.d.ts +25 -0
  72. package/dist/planning/tools/enter-plan-mode.d.ts.map +1 -0
  73. package/dist/planning/tools/enter-plan-mode.js +98 -0
  74. package/dist/planning/tools/enter-plan-mode.js.map +1 -0
  75. package/dist/planning/tools/exit-plan-mode.d.ts +24 -0
  76. package/dist/planning/tools/exit-plan-mode.d.ts.map +1 -0
  77. package/dist/planning/tools/exit-plan-mode.js +149 -0
  78. package/dist/planning/tools/exit-plan-mode.js.map +1 -0
  79. package/dist/planning/types.d.ts +100 -0
  80. package/dist/planning/types.d.ts.map +1 -0
  81. package/dist/planning/types.js +28 -0
  82. package/dist/planning/types.js.map +1 -0
  83. package/dist/pricing/calculator.d.ts +21 -0
  84. package/dist/pricing/calculator.d.ts.map +1 -0
  85. package/dist/pricing/calculator.js +59 -0
  86. package/dist/pricing/calculator.js.map +1 -0
  87. package/dist/pricing/index.d.ts +7 -0
  88. package/dist/pricing/index.d.ts.map +1 -0
  89. package/dist/pricing/index.js +7 -0
  90. package/dist/pricing/index.js.map +1 -0
  91. package/dist/pricing/models.d.ts +20 -0
  92. package/dist/pricing/models.d.ts.map +1 -0
  93. package/dist/pricing/models.js +322 -0
  94. package/dist/pricing/models.js.map +1 -0
  95. package/dist/pricing/types.d.ts +30 -0
  96. package/dist/pricing/types.d.ts.map +1 -0
  97. package/dist/pricing/types.js +5 -0
  98. package/dist/pricing/types.js.map +1 -0
  99. package/dist/providers/anthropic.d.ts.map +1 -1
  100. package/dist/providers/anthropic.js +17 -10
  101. package/dist/providers/anthropic.js.map +1 -1
  102. package/dist/providers/gemini.d.ts.map +1 -1
  103. package/dist/providers/gemini.js +21 -14
  104. package/dist/providers/gemini.js.map +1 -1
  105. package/dist/providers/openai.d.ts.map +1 -1
  106. package/dist/providers/openai.js +12 -8
  107. package/dist/providers/openai.js.map +1 -1
  108. package/dist/providers/types.d.ts +2 -0
  109. package/dist/providers/types.d.ts.map +1 -1
  110. package/dist/providers/vertex-ai.d.ts.map +1 -1
  111. package/dist/providers/vertex-ai.js +17 -10
  112. package/dist/providers/vertex-ai.js.map +1 -1
  113. package/dist/session/manager.d.ts +4 -0
  114. package/dist/session/manager.d.ts.map +1 -1
  115. package/dist/session/manager.js +8 -0
  116. package/dist/session/manager.js.map +1 -1
  117. package/dist/tools/builtin/ask-user.d.ts +64 -0
  118. package/dist/tools/builtin/ask-user.d.ts.map +1 -0
  119. package/dist/tools/builtin/ask-user.js +148 -0
  120. package/dist/tools/builtin/ask-user.js.map +1 -0
  121. package/dist/tools/index.d.ts +19 -1
  122. package/dist/tools/index.d.ts.map +1 -1
  123. package/dist/tools/index.js +11 -0
  124. package/dist/tools/index.js.map +1 -1
  125. package/dist/tools/registry.d.ts +13 -0
  126. package/dist/tools/registry.d.ts.map +1 -1
  127. package/dist/tools/registry.js +79 -2
  128. package/dist/tools/registry.js.map +1 -1
  129. package/dist/tools/types.d.ts +17 -0
  130. package/dist/tools/types.d.ts.map +1 -1
  131. package/dist/tools/types.js.map +1 -1
  132. package/docs/cost-tracking-comparison.md +904 -0
  133. package/docs/operating-modes.md +96 -0
  134. package/docs/proposals/0012-ask-user-question.md +66 -1
  135. package/docs/proposals/0025-cost-tracking.md +60 -2
  136. package/docs/proposals/README.md +2 -2
  137. package/examples/test-ask-user.ts +167 -0
  138. package/examples/test-checkpointing.ts +121 -0
  139. package/examples/test-cost-tracking.ts +77 -0
  140. package/examples/test-interrupt-cleanup.ts +94 -0
  141. package/package.json +1 -1
  142. package/src/agent/agent.ts +130 -4
  143. package/src/agent/index.ts +1 -0
  144. package/src/agent/types.ts +19 -1
  145. package/src/checkpointing/checkpoint-manager.ts +327 -0
  146. package/src/checkpointing/index.ts +45 -0
  147. package/src/checkpointing/types.ts +104 -0
  148. package/src/cli/components/App.tsx +259 -8
  149. package/src/cli/components/CommandSuggestions.tsx +5 -0
  150. package/src/cli/components/Messages.tsx +66 -4
  151. package/src/cli/components/ModeIndicator.tsx +174 -0
  152. package/src/cli/components/PlanApproval.tsx +327 -0
  153. package/src/cli/components/QuestionPrompt.tsx +462 -0
  154. package/src/cli/components/index.ts +1 -0
  155. package/src/cli/components/theme.ts +14 -1
  156. package/src/index.ts +15 -0
  157. package/src/permissions/types.ts +2 -0
  158. package/src/planning/index.ts +53 -0
  159. package/src/planning/plan-file.ts +326 -0
  160. package/src/planning/state.ts +305 -0
  161. package/src/planning/tools/enter-plan-mode.ts +111 -0
  162. package/src/planning/tools/exit-plan-mode.ts +170 -0
  163. package/src/planning/types.ts +150 -0
  164. package/src/pricing/calculator.ts +71 -0
  165. package/src/pricing/index.ts +7 -0
  166. package/src/pricing/models.ts +334 -0
  167. package/src/pricing/types.ts +32 -0
  168. package/src/prompts/system/base.txt +42 -0
  169. package/src/prompts/tools/ask-user.txt +110 -0
  170. package/src/providers/anthropic.ts +21 -10
  171. package/src/providers/gemini.ts +25 -14
  172. package/src/providers/openai.ts +17 -8
  173. package/src/providers/types.ts +3 -0
  174. package/src/providers/vertex-ai.ts +21 -10
  175. package/src/session/manager.ts +9 -0
  176. package/src/tools/builtin/ask-user.ts +185 -0
  177. package/src/tools/index.ts +23 -0
  178. package/src/tools/registry.ts +95 -2
  179. package/src/tools/types.ts +18 -0
  180. package/.gencode/settings.local.json +0 -7
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Checkpointing Module
3
+ *
4
+ * Provides automatic tracking of file changes with undo/rewind capabilities.
5
+ *
6
+ * Usage:
7
+ * import { getCheckpointManager, initCheckpointManager } from './checkpointing';
8
+ *
9
+ * // Initialize at session start
10
+ * const manager = initCheckpointManager('session-123');
11
+ *
12
+ * // Record changes (done automatically by ToolRegistry)
13
+ * manager.recordChange({
14
+ * path: '/path/to/file.ts',
15
+ * changeType: 'modify',
16
+ * previousContent: 'old content',
17
+ * newContent: 'new content',
18
+ * toolName: 'Edit'
19
+ * });
20
+ *
21
+ * // List changes
22
+ * console.log(manager.formatCheckpointList());
23
+ *
24
+ * // Rewind changes
25
+ * await manager.rewind({ all: true });
26
+ */
27
+
28
+ // Type exports
29
+ export type {
30
+ ChangeType,
31
+ FileCheckpoint,
32
+ CheckpointSession,
33
+ RewindOptions,
34
+ RewindResult,
35
+ CheckpointSummary,
36
+ RecordChangeInput,
37
+ } from './types.js';
38
+
39
+ // Manager exports
40
+ export {
41
+ CheckpointManager,
42
+ getCheckpointManager,
43
+ initCheckpointManager,
44
+ resetCheckpointManager,
45
+ } from './checkpoint-manager.js';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Checkpointing System Type Definitions
3
+ *
4
+ * Provides automatic tracking of file changes with undo/rewind capabilities.
5
+ */
6
+
7
+ /**
8
+ * Type of file change
9
+ */
10
+ export type ChangeType = 'create' | 'modify' | 'delete';
11
+
12
+ /**
13
+ * A single file checkpoint recording a change
14
+ */
15
+ export interface FileCheckpoint {
16
+ /** Unique identifier for this checkpoint */
17
+ id: string;
18
+ /** Path to the file that was changed */
19
+ path: string;
20
+ /** Type of change made */
21
+ changeType: ChangeType;
22
+ /** When the change was made */
23
+ timestamp: Date;
24
+ /** File content before the change (null for create) */
25
+ previousContent: string | null;
26
+ /** File content after the change (null for delete) */
27
+ newContent: string | null;
28
+ /** Which tool made the change */
29
+ toolName: string;
30
+ }
31
+
32
+ /**
33
+ * A checkpoint session containing all checkpoints for a session
34
+ */
35
+ export interface CheckpointSession {
36
+ /** Session ID this checkpoint session belongs to */
37
+ sessionId: string;
38
+ /** All checkpoints in order */
39
+ checkpoints: FileCheckpoint[];
40
+ /** When this checkpoint session was created */
41
+ createdAt: Date;
42
+ }
43
+
44
+ /**
45
+ * Options for rewinding changes
46
+ */
47
+ export interface RewindOptions {
48
+ /** Rewind a specific checkpoint by ID */
49
+ checkpointId?: string;
50
+ /** Rewind changes to a specific file path */
51
+ path?: string;
52
+ /** Rewind all changes */
53
+ all?: boolean;
54
+ /** Rewind the last N changes */
55
+ count?: number;
56
+ }
57
+
58
+ /**
59
+ * Result of a rewind operation
60
+ */
61
+ export interface RewindResult {
62
+ /** Whether the rewind was successful */
63
+ success: boolean;
64
+ /** Files that were successfully reverted */
65
+ revertedFiles: Array<{
66
+ path: string;
67
+ action: 'restored' | 'deleted' | 'recreated';
68
+ }>;
69
+ /** Any errors that occurred during rewind */
70
+ errors: Array<{
71
+ path: string;
72
+ error: string;
73
+ }>;
74
+ }
75
+
76
+ /**
77
+ * Summary of changes in a checkpoint session
78
+ */
79
+ export interface CheckpointSummary {
80
+ /** Number of files created */
81
+ created: number;
82
+ /** Number of files modified */
83
+ modified: number;
84
+ /** Number of files deleted */
85
+ deleted: number;
86
+ /** Total number of checkpoints */
87
+ total: number;
88
+ }
89
+
90
+ /**
91
+ * Input for recording a file change
92
+ */
93
+ export interface RecordChangeInput {
94
+ /** Path to the file */
95
+ path: string;
96
+ /** Type of change */
97
+ changeType: ChangeType;
98
+ /** Content before change (null for create) */
99
+ previousContent: string | null;
100
+ /** Content after change (null for delete) */
101
+ newContent: string | null;
102
+ /** Tool that made the change */
103
+ toolName: string;
104
+ }
@@ -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,
@@ -28,11 +30,18 @@ import {
28
30
  PermissionAuditDisplay,
29
31
  } from './PermissionPrompt.js';
30
32
  import { TodoList } from './TodoList.js';
33
+ import { QuestionPrompt, AnswerDisplay } from './QuestionPrompt.js';
31
34
  import { colors, icons } from './theme.js';
32
- import { getTodos } from '../../tools/index.js';
35
+ import { getTodos, formatAnswersForDisplay } from '../../tools/index.js';
36
+ import type { Question, QuestionAnswer } from '../../tools/types.js';
33
37
  import type { ProviderName } from '../../providers/index.js';
34
38
  import type { ApprovalAction, ApprovalSuggestion } from '../../permissions/types.js';
35
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';
36
45
 
37
46
  // Types
38
47
  interface HistoryItem {
@@ -49,6 +58,19 @@ interface ConfirmState {
49
58
  resolve: (action: ApprovalAction) => void;
50
59
  }
51
60
 
61
+ interface QuestionState {
62
+ questions: Question[];
63
+ resolve: (answers: QuestionAnswer[]) => void;
64
+ }
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
+
52
74
  interface SettingsManager {
53
75
  save: (settings: { model?: string }) => Promise<void>;
54
76
  getCwd?: () => string;
@@ -101,6 +123,9 @@ const formatRelativeTime = (dateStr: string) => {
101
123
  // ============================================================================
102
124
  function HelpPanel() {
103
125
  const commands: [string, string][] = [
126
+ ['/plan [desc]', 'Enter plan mode'],
127
+ ['/normal', 'Exit to normal mode'],
128
+ ['/accept', 'Enter auto-accept mode'],
104
129
  ['/model [name]', 'Switch model'],
105
130
  ['/provider', 'Manage providers'],
106
131
  ['/sessions', 'List sessions'],
@@ -110,6 +135,8 @@ function HelpPanel() {
110
135
  ['/clear', 'Clear chat'],
111
136
  ['/init', 'Generate AGENT.md'],
112
137
  ['/memory', 'Show memory files'],
138
+ ['/changes', 'List file changes'],
139
+ ['/rewind [n|all]', 'Undo file changes'],
113
140
  ];
114
141
 
115
142
  return (
@@ -235,6 +262,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
235
262
  const [processingStartTime, setProcessingStartTime] = useState<number | undefined>(undefined);
236
263
  const [tokenCount, setTokenCount] = useState(0);
237
264
  const [confirmState, setConfirmState] = useState<ConfirmState | null>(null);
265
+ const [questionState, setQuestionState] = useState<QuestionState | null>(null);
238
266
  const [showModelSelector, setShowModelSelector] = useState(false);
239
267
  const [showProviderManager, setShowProviderManager] = useState(false);
240
268
  const [currentModel, setCurrentModel] = useState(config.model);
@@ -244,6 +272,16 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
244
272
  const pendingToolRef = useRef<{ name: string; input: Record<string, unknown> } | null>(null);
245
273
  const [todos, setTodos] = useState<ReturnType<typeof getTodos>>([]);
246
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
+
247
285
  // Check if showing command suggestions
248
286
  const showCmdSuggestions = input.startsWith('/') && !isProcessing;
249
287
  const cmdSuggestions = showCmdSuggestions ? getFilteredCommands(input) : [];
@@ -266,6 +304,11 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
266
304
 
267
305
  // Set enhanced confirm callback with approval options
268
306
  agent.setEnhancedConfirmCallback(async (tool, toolInput, suggestions) => {
307
+ // Auto-approve in accept mode
308
+ if (currentModeRef.current === 'accept') {
309
+ return 'allow_once';
310
+ }
311
+
269
312
  return new Promise<ApprovalAction>((resolve) => {
270
313
  setConfirmState({
271
314
  tool,
@@ -276,6 +319,13 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
276
319
  });
277
320
  });
278
321
 
322
+ // Set askUser callback for AskUserQuestion tool
323
+ agent.setAskUserCallback(async (questions) => {
324
+ return new Promise<QuestionAnswer[]>((resolve) => {
325
+ setQuestionState({ questions, resolve });
326
+ });
327
+ });
328
+
279
329
  // Set callback to save permission rules to settings.local.json
280
330
  if (settingsManager?.addPermissionRule) {
281
331
  agent.setSaveRuleCallback(async (tool, pattern) => {
@@ -295,6 +345,32 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
295
345
  init();
296
346
  }, [agent, resumeLatest, addHistory, permissionSettings, settingsManager]);
297
347
 
348
+ // Handle question answers (AskUserQuestion)
349
+ const handleQuestionComplete = useCallback((answers: QuestionAnswer[]) => {
350
+ if (questionState) {
351
+ // Show confirmation in history
352
+ addHistory({
353
+ type: 'info',
354
+ content: formatAnswersForDisplay(answers),
355
+ });
356
+ questionState.resolve(answers);
357
+ setQuestionState(null);
358
+ }
359
+ }, [questionState, addHistory]);
360
+
361
+ // Handle question cancel
362
+ const handleQuestionCancel = useCallback(() => {
363
+ if (questionState) {
364
+ // Clear pending tool display (no more spinner)
365
+ pendingToolRef.current = null;
366
+ setPendingTool(null);
367
+ // Add canceled message to history
368
+ addHistory({ type: 'info', content: 'Question canceled' });
369
+ questionState.resolve([]); // Return empty answers on cancel
370
+ setQuestionState(null);
371
+ }
372
+ }, [questionState, addHistory]);
373
+
298
374
  // Handle permission decision
299
375
  const handlePermissionDecision = (action: ApprovalAction) => {
300
376
  if (confirmState) {
@@ -524,6 +600,110 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
524
600
  return true;
525
601
  }
526
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
+
527
707
  default:
528
708
  return false;
529
709
  }
@@ -620,9 +800,13 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
620
800
  streamingTextRef.current = '';
621
801
  setStreamingText('');
622
802
  }
623
- // Add completion message with duration
803
+ // Add completion message with duration and cost info
624
804
  const durationMs = Date.now() - startTime;
625
- addHistory({ type: 'completion', content: '', meta: { durationMs } });
805
+ addHistory({
806
+ type: 'completion',
807
+ content: '',
808
+ meta: { durationMs, usage: event.usage, cost: event.cost },
809
+ });
626
810
  setProcessingStartTime(undefined);
627
811
  break;
628
812
  }
@@ -730,9 +914,36 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
730
914
  setIsProcessing(false);
731
915
  setStreamingText('');
732
916
  streamingTextRef.current = '';
917
+ // Clear pending tool (stop spinner)
918
+ pendingToolRef.current = null;
919
+ setPendingTool(null);
920
+ // Clean up incomplete tool_use messages to prevent API errors
921
+ agent.cleanupIncompleteMessages();
733
922
  addHistory({ type: 'info', content: 'Interrupted' });
734
923
  }
735
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
+
736
947
  // Command suggestion navigation
737
948
  if (showCmdSuggestions && cmdSuggestions.length > 0) {
738
949
  if (key.upArrow) {
@@ -809,7 +1020,13 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
809
1020
  }
810
1021
  return <InfoMessage text={item.content} />;
811
1022
  case 'completion':
812
- 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
+ );
813
1030
  case 'todos':
814
1031
  return <TodoList todos={item.meta?.todos as ReturnType<typeof getTodos>} />;
815
1032
  default:
@@ -818,7 +1035,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
818
1035
  };
819
1036
 
820
1037
  return (
821
- <Box flexDirection="column">
1038
+ <Box flexDirection="column" paddingBottom={2}>
822
1039
  <Static items={history}>
823
1040
  {(item) => <Box key={item.id}>{renderHistoryItem(item)}</Box>}
824
1041
  </Static>
@@ -837,6 +1054,30 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
837
1054
  />
838
1055
  )}
839
1056
 
1057
+ {questionState && (
1058
+ <QuestionPrompt
1059
+ questions={questionState.questions}
1060
+ onComplete={handleQuestionComplete}
1061
+ onCancel={handleQuestionCancel}
1062
+ />
1063
+ )}
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
+
840
1081
  {showModelSelector && (
841
1082
  <Box marginTop={1}>
842
1083
  <ModelSelector
@@ -861,8 +1102,8 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
861
1102
  </Box>
862
1103
  )}
863
1104
 
864
- {!confirmState && !showModelSelector && !showProviderManager && (
865
- <Box flexDirection="column" marginTop={1}>
1105
+ {!confirmState && !questionState && !showModelSelector && !showProviderManager && (
1106
+ <Box flexDirection="column" marginTop={2}>
866
1107
  <PromptInput
867
1108
  key={inputKey}
868
1109
  value={input}
@@ -872,10 +1113,20 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
872
1113
  {showCmdSuggestions && cmdSuggestions.length > 0 && (
873
1114
  <CommandSuggestions input={input} selectedIndex={cmdSuggestionIndex} />
874
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
+ )}
875
1126
  </Box>
876
1127
  )}
877
1128
 
878
- {isProcessing && !confirmState ? (
1129
+ {isProcessing && !confirmState && !questionState ? (
879
1130
  <ProgressBar startTime={processingStartTime} tokenCount={tokenCount} isThinking={isThinking} />
880
1131
  ) : showCmdSuggestions && cmdSuggestions.length > 0 ? (
881
1132
  <Box marginTop={1}>
@@ -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) =>
@@ -126,6 +128,11 @@ function formatToolInput(name: string, input: Record<string, unknown>): string {
126
128
  const todos = input.todos as Array<{ content: string; status: string }> || [];
127
129
  return `${todos.length} task${todos.length !== 1 ? 's' : ''}`;
128
130
  }
131
+ case 'AskUserQuestion': {
132
+ // Show collapsed JSON preview
133
+ const json = JSON.stringify(input);
134
+ return truncate(json, 60);
135
+ }
129
136
  default:
130
137
  return truncate(JSON.stringify(input), 40);
131
138
  }
@@ -135,6 +142,25 @@ export function ToolCall({ name, input }: ToolCallProps) {
135
142
  // Hide TodoWrite (shown in TodoList component)
136
143
  if (name === 'TodoWrite') return null;
137
144
 
145
+ // Special display for AskUserQuestion (Claude Code style with expand hint)
146
+ if (name === 'AskUserQuestion') {
147
+ const json = JSON.stringify(input);
148
+ const displayJson = truncate(json, 70);
149
+
150
+ return (
151
+ <Box marginTop={1} flexDirection="column">
152
+ <Box>
153
+ <Text color={colors.tool}>{icons.tool}</Text>
154
+ <Text> </Text>
155
+ <Text bold>{name}</Text>
156
+ <Text color={colors.textMuted}> </Text>
157
+ <Text color={colors.textSecondary}>{displayJson}</Text>
158
+ <Text color={colors.textMuted}> ctrl+o</Text>
159
+ </Box>
160
+ </Box>
161
+ );
162
+ }
163
+
138
164
  const displayInput = formatToolInput(name, input);
139
165
 
140
166
  return (
@@ -162,6 +188,25 @@ export function PendingToolCall({ name, input }: PendingToolCallProps) {
162
188
  // Hide TodoWrite (shown in TodoList component)
163
189
  if (name === 'TodoWrite') return null;
164
190
 
191
+ // Special display for AskUserQuestion
192
+ if (name === 'AskUserQuestion') {
193
+ const json = JSON.stringify(input);
194
+ const displayJson = truncate(json, 70);
195
+
196
+ return (
197
+ <Box marginTop={1}>
198
+ <Text color={colors.tool}>
199
+ <InkSpinner type="dots" />
200
+ </Text>
201
+ <Text> </Text>
202
+ <Text bold>{name}</Text>
203
+ <Text color={colors.textMuted}> </Text>
204
+ <Text color={colors.textSecondary}>{displayJson}</Text>
205
+ <Text color={colors.textMuted}> ctrl+o</Text>
206
+ </Box>
207
+ );
208
+ }
209
+
165
210
  const displayInput = formatToolInput(name, input);
166
211
 
167
212
  return (
@@ -303,16 +348,33 @@ function formatDuration(ms: number): string {
303
348
 
304
349
  interface CompletionMessageProps {
305
350
  durationMs: number;
351
+ usage?: {
352
+ inputTokens: number;
353
+ outputTokens: number;
354
+ };
355
+ cost?: CostEstimate;
306
356
  }
307
357
 
308
- export function CompletionMessage({ durationMs }: CompletionMessageProps) {
358
+ export function CompletionMessage({ durationMs, usage, cost }: CompletionMessageProps) {
309
359
  // Pick a random verb (stable per render via useMemo would be better, but keep simple)
310
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
+
311
375
  return (
312
376
  <Box marginTop={1}>
313
- <Text color={colors.textMuted}>
314
- ✻ {verb} for {formatDuration(durationMs)}
315
- </Text>
377
+ <Text color={colors.textMuted}>{parts.join(' • ')}</Text>
316
378
  </Box>
317
379
  );
318
380
  }