centaurus-cli 3.1.2 → 3.1.4

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 (142) hide show
  1. package/dist/cli-adapter.js +689 -155
  2. package/dist/cli-adapter.js.map +1 -1
  3. package/dist/config/defaultConfig.js +1 -4
  4. package/dist/config/defaultConfig.js.map +1 -1
  5. package/dist/config/models.js +6 -0
  6. package/dist/config/models.js.map +1 -1
  7. package/dist/config/slash-commands.js +66 -2
  8. package/dist/config/slash-commands.js.map +1 -1
  9. package/dist/config/types.js +4 -4
  10. package/dist/config/types.js.map +1 -1
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/services/ai-context-injector.js +109 -0
  14. package/dist/services/ai-context-injector.js.map +1 -1
  15. package/dist/services/ai-service-client.js +3 -2
  16. package/dist/services/ai-service-client.js.map +1 -1
  17. package/dist/services/api-client.js.map +1 -1
  18. package/dist/services/background-task-manager.js +59 -0
  19. package/dist/services/background-task-manager.js.map +1 -1
  20. package/dist/services/local-chat-storage.js +2 -0
  21. package/dist/services/local-chat-storage.js.map +1 -1
  22. package/dist/services/skill-storage.js +141 -0
  23. package/dist/services/skill-storage.js.map +1 -0
  24. package/dist/services/sub-agent-manager.js +49 -8
  25. package/dist/services/sub-agent-manager.js.map +1 -1
  26. package/dist/services/warpify-detector.js +17 -5
  27. package/dist/services/warpify-detector.js.map +1 -1
  28. package/dist/tools/background-command.js +5 -2
  29. package/dist/tools/background-command.js.map +1 -1
  30. package/dist/tools/command.js +367 -109
  31. package/dist/tools/command.js.map +1 -1
  32. package/dist/tools/file-ops.js +23 -6
  33. package/dist/tools/file-ops.js.map +1 -1
  34. package/dist/tools/plan-mode.js +184 -336
  35. package/dist/tools/plan-mode.js.map +1 -1
  36. package/dist/tools/sub-agent.js +24 -5
  37. package/dist/tools/sub-agent.js.map +1 -1
  38. package/dist/tools/todo-list.js +157 -0
  39. package/dist/tools/todo-list.js.map +1 -0
  40. package/dist/types/skill.js +30 -0
  41. package/dist/types/skill.js.map +1 -0
  42. package/dist/ui/components/App.js +956 -162
  43. package/dist/ui/components/App.js.map +1 -1
  44. package/dist/ui/components/AuthScreen.js +3 -1
  45. package/dist/ui/components/AuthScreen.js.map +1 -1
  46. package/dist/ui/components/AuthWelcomeScreen.js +3 -1
  47. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  48. package/dist/ui/components/CodeBlock.js +3 -1
  49. package/dist/ui/components/CodeBlock.js.map +1 -1
  50. package/dist/ui/components/CompactShellPreview.js +44 -0
  51. package/dist/ui/components/CompactShellPreview.js.map +1 -0
  52. package/dist/ui/components/ConfigViewer.js +3 -1
  53. package/dist/ui/components/ConfigViewer.js.map +1 -1
  54. package/dist/ui/components/ConfirmPrompt.js +3 -1
  55. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  56. package/dist/ui/components/ConnectionStatusMessage.js +3 -1
  57. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  58. package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
  59. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  60. package/dist/ui/components/DiffViewer.js +6 -3
  61. package/dist/ui/components/DiffViewer.js.map +1 -1
  62. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  63. package/dist/ui/components/FileTagAutocomplete.js +4 -2
  64. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  65. package/dist/ui/components/InputBox.js +243 -40
  66. package/dist/ui/components/InputBox.js.map +1 -1
  67. package/dist/ui/components/InteractiveShell.js +5 -3
  68. package/dist/ui/components/InteractiveShell.js.map +1 -1
  69. package/dist/ui/components/KeyboardHelp.js +4 -1
  70. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  71. package/dist/ui/components/LoadingIndicator.js +3 -1
  72. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  73. package/dist/ui/components/MCPAddScreen.js +63 -13
  74. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  75. package/dist/ui/components/MarkdownRenderer.js +3 -1
  76. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  77. package/dist/ui/components/MessageDisplay.js +9 -7
  78. package/dist/ui/components/MessageDisplay.js.map +1 -1
  79. package/dist/ui/components/ModelPicker.js +170 -0
  80. package/dist/ui/components/ModelPicker.js.map +1 -0
  81. package/dist/ui/components/MonitorModeAIPanel.js +3 -1
  82. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  83. package/dist/ui/components/PlanAcceptedMessage.js +12 -6
  84. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  85. package/dist/ui/components/PlanQuestionMessage.js +37 -0
  86. package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
  87. package/dist/ui/components/PlanQuestionScreen.js +138 -0
  88. package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
  89. package/dist/ui/components/PlanReviewScreen.js +7 -9
  90. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  91. package/dist/ui/components/RulesEditorScreen.js +65 -28
  92. package/dist/ui/components/RulesEditorScreen.js.map +1 -1
  93. package/dist/ui/components/SelectPrompt.js +3 -1
  94. package/dist/ui/components/SelectPrompt.js.map +1 -1
  95. package/dist/ui/components/SkillCreatorScreen.js +217 -0
  96. package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
  97. package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
  98. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  99. package/dist/ui/components/StatusBar.js +4 -2
  100. package/dist/ui/components/StatusBar.js.map +1 -1
  101. package/dist/ui/components/StreamingMessageDisplay.js +5 -3
  102. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  103. package/dist/ui/components/SubAgentListScreen.js +65 -0
  104. package/dist/ui/components/SubAgentListScreen.js.map +1 -0
  105. package/dist/ui/components/SubAgentViewScreen.js +123 -0
  106. package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
  107. package/dist/ui/components/TaskCompletedMessage.js +40 -8
  108. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  109. package/dist/ui/components/TaskProgressIndicator.js +6 -4
  110. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  111. package/dist/ui/components/TextEditor.js +297 -0
  112. package/dist/ui/components/TextEditor.js.map +1 -0
  113. package/dist/ui/components/TodoListMessage.js +59 -0
  114. package/dist/ui/components/TodoListMessage.js.map +1 -0
  115. package/dist/ui/components/ToolExecutionMessage.js +134 -84
  116. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  117. package/dist/ui/components/ToolExecutionStatus.js +3 -1
  118. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  119. package/dist/ui/components/WelcomeBanner.js +33 -33
  120. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  121. package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
  122. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  123. package/dist/ui/theme.js +97 -0
  124. package/dist/ui/theme.js.map +1 -0
  125. package/dist/ui/utils/chat-history-limit.js +247 -0
  126. package/dist/ui/utils/chat-history-limit.js.map +1 -0
  127. package/dist/utils/chat-formatter.js +22 -9
  128. package/dist/utils/chat-formatter.js.map +1 -1
  129. package/dist/utils/git-stats.js +7 -5
  130. package/dist/utils/git-stats.js.map +1 -1
  131. package/dist/utils/input-classifier.js +11 -1
  132. package/dist/utils/input-classifier.js.map +1 -1
  133. package/dist/utils/output-truncation.js +175 -0
  134. package/dist/utils/output-truncation.js.map +1 -0
  135. package/dist/utils/rule-reference-resolver.js +3 -3
  136. package/dist/utils/rule-reference-resolver.js.map +1 -1
  137. package/dist/utils/tunnel-commands-manager.js +134 -0
  138. package/dist/utils/tunnel-commands-manager.js.map +1 -0
  139. package/package.json +91 -90
  140. package/postinstall.js +4 -11
  141. package/dist/ui/components/MultiLineInput.js +0 -255
  142. package/dist/ui/components/MultiLineInput.js.map +0 -1
@@ -3,6 +3,7 @@ import { Box, Text, useApp, useInput, Static } from "ink";
3
3
  import SelectInput from "ink-select-input";
4
4
  import { CircularSelectInput } from "./CircularSelectInput.js";
5
5
  import stripAnsi from "strip-ansi";
6
+ import { ThemeContext, deriveThemeColors, setGlobalAccentColor, DEFAULT_ACCENT_COLOR } from "../theme.js";
6
7
  import TextInput from "ink-text-input";
7
8
  import * as path from "path";
8
9
  import { quickLog } from "../../utils/conversation-logger.js";
@@ -11,6 +12,7 @@ import { InputBox } from "./InputBox.js";
11
12
  import { MessageDisplay, countFilesInMessage } from "./MessageDisplay.js";
12
13
  import { StreamingMessageDisplay } from "./StreamingMessageDisplay.js";
13
14
  import { LoadingIndicator } from "./LoadingIndicator.js";
15
+ import { ModelPicker } from "./ModelPicker.js";
14
16
  import { AgentTimer } from "./AgentTimer.js";
15
17
  import { SelectPrompt } from "./SelectPrompt.js";
16
18
  import { ConfirmPrompt } from "./ConfirmPrompt.js";
@@ -20,16 +22,24 @@ import { DiffViewer } from "./DiffViewer.js";
20
22
  import { PasswordPrompt } from "./PasswordPrompt.js";
21
23
  import { VersionUpdatePrompt } from "./VersionUpdatePrompt.js";
22
24
  import { InteractiveShell } from "./InteractiveShell.js";
25
+ import { CompactShellPreview } from "./CompactShellPreview.js";
23
26
  import { checkForUpdates } from "../../utils/version-checker.js";
24
27
  import { runInteractiveEditor, runWSLEditor, runDockerEditor, runSSHEditor, runNestedDockerSSHEditor } from "../../utils/editor-utils.js";
25
28
  import { DetailedPlanReviewScreen } from "./DetailedPlanReviewScreen.js";
26
29
  import { TaskCompletedMessage } from "./TaskCompletedMessage.js";
30
+ import { TodoListMessage } from "./TodoListMessage.js";
27
31
  import { PlanAcceptedMessage } from "./PlanAcceptedMessage.js";
32
+ import { PlanQuestionScreen } from "./PlanQuestionScreen.js";
33
+ import { PlanQuestionMessage } from "./PlanQuestionMessage.js";
28
34
  import { MCPAddScreen } from "./MCPAddScreen.js";
29
35
  import { MCPServerListScreen } from "./MCPServerListScreen.js";
30
36
  import { MCPListScreen } from "./MCPListScreen.js";
31
37
  import { WorkflowCreatorScreen } from "./WorkflowCreatorScreen.js";
32
38
  import { RulesEditorScreen } from "./RulesEditorScreen.js";
39
+ import { SkillCreatorScreen } from "./SkillCreatorScreen.js";
40
+ import { SubAgentListScreen } from "./SubAgentListScreen.js";
41
+ import { SubAgentViewScreen } from "./SubAgentViewScreen.js";
42
+ import { SubAgentManager } from "../../services/sub-agent-manager.js";
33
43
  import { processTerminalOutput } from "../../utils/terminal-output.js";
34
44
  import { BackgroundTaskManager } from "../../services/background-task-manager.js";
35
45
  import { MonitorModeAIPanel } from "./MonitorModeAIPanel.js";
@@ -44,6 +54,9 @@ import { logDebug, logError } from "../../utils/logger.js";
44
54
  import { getTerminalDimensions } from "../../hooks/useTerminalDimensions.js";
45
55
  import { ConfigManager } from "../../config/manager.js";
46
56
  import { getGitDiffStats, getRemoteGitDiffStats } from "../../utils/git-stats.js";
57
+ import { limitChatHistoryForDisplay } from "../utils/chat-history-limit.js";
58
+ import { TunnelCommandsManager } from "../../utils/tunnel-commands-manager.js";
59
+ import { detectIntent } from "../../utils/input-classifier.js";
47
60
  function isOutsideCwd(filePath, cwd) {
48
61
  if (!filePath || !cwd) return false;
49
62
  try {
@@ -54,6 +67,11 @@ function isOutsideCwd(filePath, cwd) {
54
67
  return false;
55
68
  }
56
69
  }
70
+ const ScreenContainer = ({ children }) => {
71
+ const rows = process.stdout.rows || 24;
72
+ const maxContentHeight = Math.max(3, rows - 2);
73
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", height: maxContentHeight, overflow: "hidden" }, children), /* @__PURE__ */ React.createElement(Box, { justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "[truncated]")));
74
+ };
57
75
  const BANNER_ITEM = { id: "__banner__", role: "__banner__", content: "", timestamp: /* @__PURE__ */ new Date(0) };
58
76
  const MessageList = React.memo(({ history, current, showBanner }) => {
59
77
  const staticItems = React.useMemo(() => {
@@ -83,10 +101,23 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
83
101
  TaskCompletedMessage,
84
102
  {
85
103
  key: item.id,
86
- taskNumber: msg.taskCompletion.taskNumber,
87
- totalTasks: msg.taskCompletion.totalTasks,
88
- taskDescription: msg.taskCompletion.taskDescription,
89
- completionNote: msg.taskCompletion.completionNote
104
+ completedStepNumber: msg.taskCompletion.completedStepNumber,
105
+ completedStepDescription: msg.taskCompletion.completedStepDescription,
106
+ completedCount: msg.taskCompletion.completedCount,
107
+ totalCount: msg.taskCompletion.totalCount,
108
+ completionNote: msg.taskCompletion.completionNote,
109
+ allSteps: msg.taskCompletion.allSteps
110
+ }
111
+ );
112
+ }
113
+ if (msg.todoList) {
114
+ return /* @__PURE__ */ React.createElement(
115
+ TodoListMessage,
116
+ {
117
+ key: item.id,
118
+ todos: msg.todoList.todos,
119
+ completedCount: msg.todoList.completedCount,
120
+ totalCount: msg.todoList.totalCount
90
121
  }
91
122
  );
92
123
  }
@@ -96,22 +127,35 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
96
127
  {
97
128
  key: item.id,
98
129
  planTitle: msg.planAccepted.planTitle,
99
- totalTasks: msg.planAccepted.totalTasks,
100
- tasks: msg.planAccepted.tasks
130
+ totalSteps: msg.planAccepted.totalSteps,
131
+ steps: msg.planAccepted.steps
132
+ }
133
+ );
134
+ }
135
+ if (msg.planQuestion) {
136
+ return /* @__PURE__ */ React.createElement(
137
+ PlanQuestionMessage,
138
+ {
139
+ key: item.id,
140
+ question: msg.planQuestion.question,
141
+ options: msg.planQuestion.options,
142
+ userAnswer: msg.planQuestion.userAnswer,
143
+ wasSkipped: msg.planQuestion.wasSkipped
101
144
  }
102
145
  );
103
146
  }
104
147
  return /* @__PURE__ */ React.createElement(MessageDisplay, { key: item.id, message: item, fileCountBefore });
105
148
  }), current && !history.some((msg) => msg.id === current.id) && (() => {
106
149
  const dimensions = getTerminalDimensions();
107
- const canStream = current.role === "assistant" && current.shouldStream !== false && dimensions.shouldEnableStreaming;
150
+ const hasActiveThoughts = current.thoughts && current.thoughts.length > 0 && current.thinkingDuration === void 0;
151
+ const canStream = current.role === "assistant" && current.shouldStream !== false && (dimensions.shouldEnableStreaming || hasActiveThoughts);
108
152
  if (canStream) {
109
153
  return /* @__PURE__ */ React.createElement(
110
154
  StreamingMessageDisplay,
111
155
  {
112
156
  key: current.id,
113
157
  message: current,
114
- maxLines: dimensions.maxStreamingLines
158
+ maxLines: dimensions.shouldEnableStreaming ? dimensions.maxStreamingLines : 1
115
159
  }
116
160
  );
117
161
  } else {
@@ -155,12 +199,13 @@ const ApprovalSection = React.memo(({ approvalRequest, onApprove }) => {
155
199
  });
156
200
  const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
157
201
  const [value, setValue] = React.useState(currentTitle);
202
+ const _theme = React.useContext(ThemeContext);
158
203
  useInput((input, key) => {
159
204
  if (key.escape) {
160
205
  onCancel();
161
206
  }
162
207
  });
163
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Rename Chat"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Current name: ", /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, currentTitle))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "New name: "), /* @__PURE__ */ React.createElement(
208
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: _theme.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: _theme.accent, bold: true }, "\u270F\uFE0F Rename Chat"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Current name: ", /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, currentTitle))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "New name: "), /* @__PURE__ */ React.createElement(
164
209
  TextInput,
165
210
  {
166
211
  value,
@@ -200,10 +245,13 @@ const App = ({
200
245
  onPlanModeChange,
201
246
  onPlanApprovalRequest,
202
247
  onPlanCreated,
248
+ onPlanQuestionRequest,
203
249
  onTaskCompleted,
250
+ onTodoListUpdate,
204
251
  onFileChangeSummary,
205
252
  onCommandModeChange,
206
253
  onToggleCommandMode,
254
+ onTogglePlanMode,
207
255
  onBackgroundModeChange,
208
256
  onToggleBackgroundMode,
209
257
  onBackgroundTaskCountChange,
@@ -248,7 +296,16 @@ const App = ({
248
296
  onPromptAnswered,
249
297
  getMainConversation,
250
298
  onWarpifySession,
299
+ onCustomTunnelStateChange,
251
300
  onAiAutoSuggestChange,
301
+ onLimitChatHistoryChange,
302
+ onThemeColorChange,
303
+ onSubAgentListScreenSetup,
304
+ onSubAgentViewScreenSetup,
305
+ onSubAgentTerminateScreenSetup,
306
+ onSkillEditorScreenSetup,
307
+ onSkillSave,
308
+ onSkillExecute,
252
309
  onWorkflowCreatorSetup,
253
310
  onWorkflowSave,
254
311
  onRulesEditorSetup,
@@ -283,6 +340,10 @@ const App = ({
283
340
  return getModelContextWindowSync(model);
284
341
  } catch (error) {
285
342
  if (model.toLowerCase().includes("kimi")) return 128e3;
343
+ if (model.toLowerCase().includes("qwen")) return 131072;
344
+ if (model.toLowerCase().includes("gpt-oss")) return 128e3;
345
+ if (model.toLowerCase().includes("nemotron")) return 131072;
346
+ if (model.toLowerCase().includes("deepseek")) return 131072;
286
347
  if (model.toLowerCase().includes("gemini")) return 1e6;
287
348
  return 1e6;
288
349
  }
@@ -320,6 +381,7 @@ const App = ({
320
381
  maxTokens: getMaxTokensForModel(initialModel || "gemini-2.5-flash"),
321
382
  contextLimitReached: false,
322
383
  shellState: void 0,
384
+ customTunnel: void 0,
323
385
  isInteractiveEditorMode: false,
324
386
  pickerOptions: void 0,
325
387
  approvalRequest: void 0,
@@ -331,6 +393,8 @@ const App = ({
331
393
  sessionQuotaExhausted: false,
332
394
  sessionQuotaTimeRemaining: "",
333
395
  aiAutoSuggest: false,
396
+ limitChatHistory: true,
397
+ themeColor: "#00ccff",
334
398
  rulesEditorRequest: void 0,
335
399
  interruptQueue: [],
336
400
  commandQueue: [],
@@ -340,22 +404,44 @@ const App = ({
340
404
  lastFileChangeSummary: null
341
405
  });
342
406
  const lastTerminalWidthRef = React.useRef(process.stdout.columns || 80);
407
+ const lastTerminalHeightRef = React.useRef(process.stdout.rows || 24);
408
+ const shellFocusedRef = React.useRef(false);
343
409
  const isCleaningRef = React.useRef(false);
344
410
  const savedHistoryRef = React.useRef([]);
345
411
  const savedCurrentRef = React.useRef(null);
412
+ const previousHiddenMessageCountRef = React.useRef(null);
346
413
  const resizeDebounceRef = React.useRef(null);
347
414
  const restoreMessagesTimeoutRef = React.useRef(null);
348
415
  const preservedInputTextRef = React.useRef("");
416
+ React.useEffect(() => {
417
+ shellFocusedRef.current = !!state.shellState?.isFocused;
418
+ }, [state.shellState?.isFocused]);
349
419
  React.useEffect(() => {
350
420
  const DEBOUNCE_MS = 300;
351
421
  const handleResize = () => {
352
422
  const newWidth = process.stdout.columns || 80;
423
+ const newHeight = process.stdout.rows || 24;
353
424
  if (resizeDebounceRef.current) {
354
425
  clearTimeout(resizeDebounceRef.current);
355
426
  }
356
427
  resizeDebounceRef.current = setTimeout(() => {
357
428
  const oldWidth = lastTerminalWidthRef.current;
358
- if (newWidth !== oldWidth && !isCleaningRef.current) {
429
+ const oldHeight = lastTerminalHeightRef.current;
430
+ const heightChanged = newHeight !== oldHeight;
431
+ const widthChanged = newWidth !== oldWidth;
432
+ lastTerminalHeightRef.current = newHeight;
433
+ if (shellFocusedRef.current && (heightChanged || widthChanged)) {
434
+ lastTerminalWidthRef.current = newWidth;
435
+ clearScreen();
436
+ return;
437
+ }
438
+ if (altScreenActiveRef.current && (heightChanged || widthChanged)) {
439
+ lastTerminalWidthRef.current = newWidth;
440
+ process.stdout.write("\x1B[2J\x1B[H");
441
+ setState((prev) => ({ ...prev, messageListKey: (prev.messageListKey ?? 0) + 1 }));
442
+ return;
443
+ }
444
+ if (widthChanged && !isCleaningRef.current) {
359
445
  lastTerminalWidthRef.current = newWidth;
360
446
  isCleaningRef.current = true;
361
447
  clearScreen();
@@ -425,8 +511,13 @@ const App = ({
425
511
  const generalConfig = configManager.load();
426
512
  setState((prev) => ({
427
513
  ...prev,
428
- aiAutoSuggest: generalConfig.aiAutoSuggest === true
514
+ aiAutoSuggest: generalConfig.aiAutoSuggest === true,
515
+ limitChatHistory: generalConfig.limitChatHistory !== false,
516
+ themeColor: generalConfig.themeColor || DEFAULT_ACCENT_COLOR
429
517
  }));
518
+ if (generalConfig.themeColor) {
519
+ setGlobalAccentColor(generalConfig.themeColor);
520
+ }
430
521
  const { quickLog: quickLog2 } = await import("../../utils/conversation-logger.js");
431
522
  quickLog2(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] loadModelsConfig effect starting, currentModel: ${initialModel}
432
523
  `);
@@ -567,6 +658,11 @@ const App = ({
567
658
  const ignoreIncomingAIUpdatesRef = React.useRef(false);
568
659
  const warpifyInProgressRef = React.useRef(false);
569
660
  const warpifyTerminatingCommandRef = React.useRef(null);
661
+ const tunnelOutputPositionRef = React.useRef(0);
662
+ const tunnelOutputFlushTimerRef = React.useRef(null);
663
+ const latestShellOutputRef = React.useRef("");
664
+ const customTunnelRef = React.useRef(void 0);
665
+ const shellRunningRef = React.useRef(false);
570
666
  const isStreamingRef = React.useRef(false);
571
667
  const bufferedContentRef = React.useRef("");
572
668
  const bufferedMessageIdRef = React.useRef(null);
@@ -579,7 +675,7 @@ const App = ({
579
675
  if (!enableStreaming) {
580
676
  bufferedContentRef.current += chunk;
581
677
  setState((prev) => {
582
- if (prev.currentMessage && prev.currentMessage.role === "assistant" && prev.currentMessage.shouldStream === false) {
678
+ if (prev.currentMessage && prev.currentMessage.role === "assistant") {
583
679
  return {
584
680
  ...prev,
585
681
  isAiWorking: true
@@ -666,6 +762,8 @@ const App = ({
666
762
  React.useEffect(() => {
667
763
  onThoughtStream((thought) => {
668
764
  if (ignoreIncomingAIUpdatesRef.current) return;
765
+ const thoughtDimensions = getTerminalDimensions();
766
+ const maxThoughtLines = thoughtDimensions.shouldEnableStreaming ? 3 : 1;
669
767
  try {
670
768
  quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Received thought chunk (${thought.length} chars): ${thought.substring(0, 100)}
671
769
  `);
@@ -682,7 +780,7 @@ const App = ({
682
780
  }
683
781
  thoughtAccumulatorRef.current = thought;
684
782
  const allLines2 = thoughtAccumulatorRef.current.split("\n").filter((line) => line.trim());
685
- const last3Lines2 = allLines2.slice(-3);
783
+ const lastNLines2 = allLines2.slice(-maxThoughtLines);
686
784
  const now = /* @__PURE__ */ new Date();
687
785
  const newMessage = {
688
786
  id: `assistant-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
@@ -691,7 +789,7 @@ const App = ({
691
789
  // Empty content initially, will be filled when text arrives
692
790
  timestamp: now,
693
791
  shouldStream: true,
694
- thoughts: last3Lines2
792
+ thoughts: lastNLines2
695
793
  };
696
794
  let newHistory = prev.messageHistory;
697
795
  if (prev.currentMessage && prev.currentMessage.role === "assistant" && prev.currentMessage.thinkingDuration !== void 0 && prev.currentMessage.content.trim() !== "") {
@@ -715,9 +813,9 @@ const App = ({
715
813
  }
716
814
  thoughtAccumulatorRef.current += thought;
717
815
  const allLines = thoughtAccumulatorRef.current.split("\n").filter((line) => line.trim());
718
- const last3Lines = allLines.slice(-3);
816
+ const lastNLines = allLines.slice(-maxThoughtLines);
719
817
  try {
720
- quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Accumulated ${allLines.length} total lines, showing last 3: ${JSON.stringify(last3Lines)}
818
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Accumulated ${allLines.length} total lines, showing last ${maxThoughtLines}: ${JSON.stringify(lastNLines)}
721
819
  `);
722
820
  } catch (e) {
723
821
  }
@@ -725,7 +823,7 @@ const App = ({
725
823
  ...prev,
726
824
  currentMessage: {
727
825
  ...prev.currentMessage,
728
- thoughts: last3Lines,
826
+ thoughts: lastNLines,
729
827
  thinkingDuration: void 0
730
828
  // Clear duration while thinking
731
829
  }
@@ -801,6 +899,63 @@ const App = ({
801
899
  };
802
900
  });
803
901
  }, [shouldSkipMessage]);
902
+ const getRegisteredTunnelCommands = useCallback(() => {
903
+ const tunnelCommandsManager = TunnelCommandsManager.getInstance();
904
+ tunnelCommandsManager.loadSync();
905
+ return tunnelCommandsManager.listCommands();
906
+ }, []);
907
+ React.useEffect(() => {
908
+ latestShellOutputRef.current = state.shellState?.output || "";
909
+ }, [state.shellState?.output]);
910
+ React.useEffect(() => {
911
+ customTunnelRef.current = state.customTunnel;
912
+ shellRunningRef.current = !!state.shellState?.isRunning;
913
+ }, [state.customTunnel, state.shellState?.isRunning]);
914
+ const flushTunnelOutput = useCallback(() => {
915
+ const currentOutput = latestShellOutputRef.current;
916
+ const lastPos = tunnelOutputPositionRef.current;
917
+ if (currentOutput.length <= lastPos) return;
918
+ const delta = currentOutput.substring(lastPos);
919
+ tunnelOutputPositionRef.current = currentOutput.length;
920
+ const processedDelta = processTerminalOutput(delta);
921
+ if (!processedDelta.trim()) return;
922
+ setState((prev) => {
923
+ if (!prev.customTunnel) return prev;
924
+ return {
925
+ ...prev,
926
+ messageHistory: [...prev.messageHistory, {
927
+ id: `tunnel-output-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
928
+ role: "tool",
929
+ content: "",
930
+ timestamp: /* @__PURE__ */ new Date(),
931
+ toolExecution: {
932
+ toolName: "execute_command",
933
+ status: "completed",
934
+ result: processedDelta,
935
+ arguments: {
936
+ command: prev.customTunnel.shellCommand,
937
+ CommandLine: prev.customTunnel.shellCommand,
938
+ cwd: prev.shellState?.cwd || process.cwd()
939
+ }
940
+ }
941
+ }]
942
+ };
943
+ });
944
+ }, []);
945
+ React.useEffect(() => {
946
+ if (!state.customTunnel || !state.shellState?.output) return;
947
+ if (tunnelOutputFlushTimerRef.current) {
948
+ clearTimeout(tunnelOutputFlushTimerRef.current);
949
+ }
950
+ tunnelOutputFlushTimerRef.current = setTimeout(() => {
951
+ flushTunnelOutput();
952
+ }, 600);
953
+ return () => {
954
+ if (tunnelOutputFlushTimerRef.current) {
955
+ clearTimeout(tunnelOutputFlushTimerRef.current);
956
+ }
957
+ };
958
+ }, [state.shellState?.output, state.customTunnel, flushTunnelOutput]);
804
959
  React.useEffect(() => {
805
960
  onDirectMessage((message) => {
806
961
  if (ignoreIncomingAIUpdatesRef.current) return;
@@ -931,7 +1086,7 @@ const App = ({
931
1086
  const pendingShellRef = React.useRef(null);
932
1087
  React.useEffect(() => {
933
1088
  onPickerSetup((options) => {
934
- clearScreen();
1089
+ enterAltScreen();
935
1090
  setState((prev) => ({
936
1091
  ...prev,
937
1092
  screen: "picker",
@@ -942,7 +1097,7 @@ const App = ({
942
1097
  }, []);
943
1098
  React.useEffect(() => {
944
1099
  onChatPickerSetup((chats, currentChatId) => {
945
- clearScreen();
1100
+ enterAltScreen();
946
1101
  setState((prev) => ({
947
1102
  ...prev,
948
1103
  screen: "chat-picker",
@@ -954,7 +1109,7 @@ const App = ({
954
1109
  }, []);
955
1110
  React.useEffect(() => {
956
1111
  onChatDeletePickerSetup((chats, currentChatId) => {
957
- clearScreen();
1112
+ enterAltScreen();
958
1113
  setState((prev) => ({
959
1114
  ...prev,
960
1115
  screen: "chat-delete-picker",
@@ -967,7 +1122,7 @@ const App = ({
967
1122
  }, []);
968
1123
  React.useEffect(() => {
969
1124
  onChatListSetup((chats, currentChatId) => {
970
- clearScreen();
1125
+ enterAltScreen();
971
1126
  setState((prev) => ({
972
1127
  ...prev,
973
1128
  screen: "chat-list-view",
@@ -979,7 +1134,7 @@ const App = ({
979
1134
  }, []);
980
1135
  React.useEffect(() => {
981
1136
  onChatRenamePickerSetup((chats, currentChatId) => {
982
- clearScreen();
1137
+ enterAltScreen();
983
1138
  setState((prev) => ({
984
1139
  ...prev,
985
1140
  screen: "chat-rename-picker",
@@ -1021,6 +1176,52 @@ const App = ({
1021
1176
  }));
1022
1177
  });
1023
1178
  }, [onAiAutoSuggestChange]);
1179
+ React.useEffect(() => {
1180
+ onLimitChatHistoryChange((enabled) => {
1181
+ setState((prev) => ({
1182
+ ...prev,
1183
+ limitChatHistory: enabled
1184
+ }));
1185
+ });
1186
+ }, [onLimitChatHistoryChange]);
1187
+ React.useEffect(() => {
1188
+ onThemeColorChange((color) => {
1189
+ setGlobalAccentColor(color);
1190
+ setState((prev) => ({ ...prev, themeColor: color }));
1191
+ });
1192
+ }, [onThemeColorChange]);
1193
+ React.useEffect(() => {
1194
+ onSubAgentListScreenSetup((agents) => {
1195
+ enterAltScreen();
1196
+ setState((prev) => ({
1197
+ ...prev,
1198
+ screen: "sub-agent-list",
1199
+ subAgentListData: agents
1200
+ }));
1201
+ });
1202
+ }, [onSubAgentListScreenSetup]);
1203
+ React.useEffect(() => {
1204
+ onSubAgentViewScreenSetup((agentId) => {
1205
+ enterAltScreen();
1206
+ setState((prev) => ({ ...prev, screen: "sub-agent-view", viewingSubAgentId: agentId }));
1207
+ });
1208
+ }, [onSubAgentViewScreenSetup]);
1209
+ React.useEffect(() => {
1210
+ onSubAgentTerminateScreenSetup((agents) => {
1211
+ enterAltScreen();
1212
+ setState((prev) => ({
1213
+ ...prev,
1214
+ screen: "sub-agent-terminate",
1215
+ subAgentListData: agents.map((a) => ({ ...a, turnCount: 0, fileOpsCount: 0 }))
1216
+ }));
1217
+ });
1218
+ }, [onSubAgentTerminateScreenSetup]);
1219
+ React.useEffect(() => {
1220
+ onSkillEditorScreenSetup((request) => {
1221
+ enterAltScreen();
1222
+ setState((prev) => ({ ...prev, screen: "skill-editor", skillEditorRequest: request }));
1223
+ });
1224
+ }, [onSkillEditorScreenSetup]);
1024
1225
  React.useEffect(() => {
1025
1226
  onSetAutoModeSetup((enabled) => {
1026
1227
  if (setAutoModeCallbackRef.current) {
@@ -1112,7 +1313,7 @@ const App = ({
1112
1313
  }, []);
1113
1314
  React.useEffect(() => {
1114
1315
  onBackgroundTaskListSetup((tasks) => {
1115
- clearScreen();
1316
+ enterAltScreen();
1116
1317
  setState((prev) => ({
1117
1318
  ...prev,
1118
1319
  screen: "background-task-list",
@@ -1123,7 +1324,7 @@ const App = ({
1123
1324
  }, []);
1124
1325
  React.useEffect(() => {
1125
1326
  onBackgroundTaskCancelSetup((tasks) => {
1126
- clearScreen();
1327
+ enterAltScreen();
1127
1328
  setState((prev) => ({
1128
1329
  ...prev,
1129
1330
  screen: "background-task-cancel",
@@ -1220,7 +1421,7 @@ const App = ({
1220
1421
  }, []);
1221
1422
  React.useEffect(() => {
1222
1423
  onMCPAddScreenSetup(() => {
1223
- clearScreen();
1424
+ enterAltScreen();
1224
1425
  setState((prev) => ({
1225
1426
  ...prev,
1226
1427
  screen: "mcp-add",
@@ -1230,7 +1431,7 @@ const App = ({
1230
1431
  }, []);
1231
1432
  React.useEffect(() => {
1232
1433
  onWorkflowCreatorSetup((initialSteps) => {
1233
- clearScreen();
1434
+ enterAltScreen();
1234
1435
  setState((prev) => ({
1235
1436
  ...prev,
1236
1437
  screen: "workflow-creator",
@@ -1241,7 +1442,7 @@ const App = ({
1241
1442
  }, []);
1242
1443
  React.useEffect(() => {
1243
1444
  onRulesEditorSetup((request) => {
1244
- clearScreen();
1445
+ enterAltScreen();
1245
1446
  setState((prev) => ({
1246
1447
  ...prev,
1247
1448
  screen: "rules-editor",
@@ -1252,7 +1453,7 @@ const App = ({
1252
1453
  }, []);
1253
1454
  React.useEffect(() => {
1254
1455
  onMCPRemoveScreenSetup((servers) => {
1255
- clearScreen();
1456
+ enterAltScreen();
1256
1457
  setState((prev) => ({
1257
1458
  ...prev,
1258
1459
  screen: "mcp-remove",
@@ -1263,7 +1464,7 @@ const App = ({
1263
1464
  }, []);
1264
1465
  React.useEffect(() => {
1265
1466
  onMCPEnableScreenSetup((servers) => {
1266
- clearScreen();
1467
+ enterAltScreen();
1267
1468
  setState((prev) => ({
1268
1469
  ...prev,
1269
1470
  screen: "mcp-enable",
@@ -1274,7 +1475,7 @@ const App = ({
1274
1475
  }, []);
1275
1476
  React.useEffect(() => {
1276
1477
  onMCPDisableScreenSetup((servers) => {
1277
- clearScreen();
1478
+ enterAltScreen();
1278
1479
  setState((prev) => ({
1279
1480
  ...prev,
1280
1481
  screen: "mcp-disable",
@@ -1285,7 +1486,7 @@ const App = ({
1285
1486
  }, []);
1286
1487
  React.useEffect(() => {
1287
1488
  onMCPListScreenSetup((servers) => {
1288
- clearScreen();
1489
+ enterAltScreen();
1289
1490
  setState((prev) => ({
1290
1491
  ...prev,
1291
1492
  screen: "mcp-list",
@@ -1294,6 +1495,27 @@ const App = ({
1294
1495
  }));
1295
1496
  });
1296
1497
  }, []);
1498
+ const ALT_SCREEN_EXCLUDED = ["chat", "version-update", "approval"];
1499
+ const altScreenActiveRef = React.useRef(false);
1500
+ const enterAltScreen = React.useCallback(() => {
1501
+ if (!altScreenActiveRef.current) {
1502
+ process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
1503
+ altScreenActiveRef.current = true;
1504
+ }
1505
+ }, []);
1506
+ const exitAltScreen = React.useCallback(() => {
1507
+ if (altScreenActiveRef.current) {
1508
+ process.stdout.write("\x1B[?1049l");
1509
+ altScreenActiveRef.current = false;
1510
+ }
1511
+ }, []);
1512
+ React.useEffect(() => {
1513
+ if (ALT_SCREEN_EXCLUDED.includes(state.screen) && altScreenActiveRef.current) {
1514
+ exitAltScreen();
1515
+ clearScreen();
1516
+ setState((prev) => ({ ...prev, messageListKey: (prev.messageListKey ?? 0) + 1 }));
1517
+ }
1518
+ }, [state.screen, exitAltScreen, clearScreen]);
1297
1519
  React.useEffect(() => {
1298
1520
  onUIMessageHistoryUpdate(state.messageHistory);
1299
1521
  }, [state.messageHistory]);
@@ -1307,25 +1529,92 @@ const App = ({
1307
1529
  `);
1308
1530
  } catch (e) {
1309
1531
  }
1310
- if (update.status === "executing") {
1311
- if (update.arguments?.shell_input) {
1532
+ if (update.arguments?._customTunnelExec) {
1533
+ if (update.status === "executing") {
1312
1534
  setState((prev) => ({
1313
1535
  ...prev,
1314
- // Move current message to history if needed
1315
1536
  messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage) ? [...prev.messageHistory, prev.currentMessage] : prev.messageHistory,
1316
- // Update current message to show the tool execution
1317
- currentMessage: {
1318
- id: `tool-execute_command-${Date.now()}`,
1537
+ currentMessage: null,
1538
+ isLoading: false,
1539
+ isAiWorking: true
1540
+ }));
1541
+ return;
1542
+ }
1543
+ if (update.status === "completed" || update.status === "error") {
1544
+ const tunnelResult = update.result || "(no output)";
1545
+ const tunnelError = update.error;
1546
+ setState((prev) => ({
1547
+ ...prev,
1548
+ messageHistory: [...prev.messageHistory, {
1549
+ id: `tunnel-exec-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
1319
1550
  role: "tool",
1320
1551
  content: "",
1321
1552
  timestamp: /* @__PURE__ */ new Date(),
1322
- toolExecution: { ...update }
1323
- },
1324
- isLoading: false,
1553
+ toolExecution: {
1554
+ toolName: "execute_command",
1555
+ status: update.status,
1556
+ result: tunnelResult,
1557
+ error: tunnelError,
1558
+ arguments: update.arguments
1559
+ }
1560
+ }],
1561
+ currentMessage: null,
1325
1562
  isAiWorking: true
1563
+ // AI loop continues
1326
1564
  }));
1327
1565
  return;
1328
1566
  }
1567
+ }
1568
+ if (update.status === "executing") {
1569
+ if (bufferedContentRef.current.length > 0) {
1570
+ const flushedContent = bufferedContentRef.current;
1571
+ bufferedContentRef.current = "";
1572
+ bufferedMessageIdRef.current = null;
1573
+ setState((prev) => {
1574
+ if (prev.currentMessage && prev.currentMessage.role === "assistant") {
1575
+ const flushedMessage = {
1576
+ ...prev.currentMessage,
1577
+ content: prev.currentMessage.content.trim() === "" ? flushedContent : prev.currentMessage.content + flushedContent,
1578
+ shouldStream: false
1579
+ };
1580
+ return {
1581
+ ...prev,
1582
+ messageHistory: shouldSkipMessage(flushedMessage) ? prev.messageHistory : [...prev.messageHistory, flushedMessage],
1583
+ currentMessage: null
1584
+ };
1585
+ }
1586
+ return prev;
1587
+ });
1588
+ }
1589
+ if (update.arguments?.shell_input) {
1590
+ setState((prev) => {
1591
+ if (prev.customTunnel) {
1592
+ return {
1593
+ ...prev,
1594
+ messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage) ? [...prev.messageHistory, prev.currentMessage] : prev.messageHistory,
1595
+ currentMessage: null,
1596
+ isLoading: false,
1597
+ isAiWorking: true
1598
+ };
1599
+ }
1600
+ return {
1601
+ ...prev,
1602
+ // Move current message to history if needed
1603
+ messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage) ? [...prev.messageHistory, prev.currentMessage] : prev.messageHistory,
1604
+ // Update current message to show the tool execution
1605
+ currentMessage: {
1606
+ id: `tool-execute_command-${Date.now()}`,
1607
+ role: "tool",
1608
+ content: "",
1609
+ timestamp: /* @__PURE__ */ new Date(),
1610
+ toolExecution: { ...update }
1611
+ },
1612
+ isLoading: false,
1613
+ isAiWorking: true
1614
+ };
1615
+ });
1616
+ return;
1617
+ }
1329
1618
  const command = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || "";
1330
1619
  const timeoutId = setTimeout(() => {
1331
1620
  if (pendingShellRef.current) {
@@ -1459,17 +1748,27 @@ const App = ({
1459
1748
  ShellInputAgent.terminateSession(prev.shellState.shellId);
1460
1749
  }
1461
1750
  }
1751
+ const keepShellState = isShellInput && !isDeadShellError;
1752
+ const tunnelClosedMessage = prev.customTunnel && !keepShellState ? {
1753
+ id: `custom-tunnel-closed-${Date.now()}`,
1754
+ role: "assistant",
1755
+ content: `Custom tunnel closed for \`${prev.customTunnel.matchedCommand}\`. Chat input will now go back to the AI.`,
1756
+ timestamp: /* @__PURE__ */ new Date(),
1757
+ shouldStream: false
1758
+ } : null;
1759
+ const skipShellMessage = keepShellState && prev.customTunnel;
1462
1760
  return {
1463
1761
  ...prev,
1464
1762
  // If it's just shell input, preserve the shell state UNLESS it's a dead shell error
1465
- shellState: isShellInput && !isDeadShellError ? prev.shellState : void 0,
1466
- messageHistory: [...prev.messageHistory, shellMessage],
1763
+ shellState: keepShellState ? prev.shellState : void 0,
1764
+ customTunnel: keepShellState ? prev.customTunnel : void 0,
1765
+ messageHistory: skipShellMessage ? prev.messageHistory : tunnelClosedMessage ? [...prev.messageHistory, tunnelClosedMessage, shellMessage] : [...prev.messageHistory, shellMessage],
1467
1766
  currentMessage: null,
1468
1767
  // Determine if AI should keep working:
1469
1768
  // - Normal shell input: preserve state (keep thrusting if in chain)
1470
1769
  // - Agent-started shell completed successfully: preserve state (agent loop continues)
1471
1770
  // - Dead shell error or manual shell completed: STOP thrusting (reset to false)
1472
- isAiWorking: isCommandModeExecution ? false : isShellInput && !isDeadShellError ? prev.isAiWorking : isAgentStarted && !shouldKill ? prev.isAiWorking : false
1771
+ isAiWorking: isCommandModeExecution ? false : keepShellState ? prev.isAiWorking : isAgentStarted && !shouldKill ? prev.isAiWorking : false
1473
1772
  };
1474
1773
  });
1475
1774
  return;
@@ -1545,9 +1844,18 @@ const App = ({
1545
1844
  let newHistory2 = prev.messageHistory;
1546
1845
  let pendingThinkingDuration = void 0;
1547
1846
  if (prev.currentMessage) {
1548
- const isThoughtOnlyMessage = prev.currentMessage.role === "assistant" && prev.currentMessage.content.trim() === "" && prev.currentMessage.thinkingDuration !== void 0;
1847
+ let messageToCommit = prev.currentMessage;
1848
+ if (bufferedContentRef.current.length > 0 && messageToCommit.role === "assistant" && messageToCommit.content.trim() === "") {
1849
+ messageToCommit = {
1850
+ ...messageToCommit,
1851
+ content: bufferedContentRef.current
1852
+ };
1853
+ bufferedContentRef.current = "";
1854
+ bufferedMessageIdRef.current = null;
1855
+ }
1856
+ const isThoughtOnlyMessage = messageToCommit.role === "assistant" && messageToCommit.content.trim() === "" && messageToCommit.thinkingDuration !== void 0;
1549
1857
  if (isThoughtOnlyMessage) {
1550
- pendingThinkingDuration = prev.currentMessage.thinkingDuration;
1858
+ pendingThinkingDuration = messageToCommit.thinkingDuration;
1551
1859
  try {
1552
1860
  quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Skipping thought-only message (no content), saving thinkingDuration ${pendingThinkingDuration}s for tool ${update.toolName}
1553
1861
  `);
@@ -1555,11 +1863,11 @@ const App = ({
1555
1863
  }
1556
1864
  } else {
1557
1865
  try {
1558
- quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Moving current message (${prev.currentMessage.role}) to history for new tool ${update.toolName}
1866
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Moving current message (${messageToCommit.role}) to history for new tool ${update.toolName}
1559
1867
  `);
1560
1868
  } catch (e) {
1561
1869
  }
1562
- newHistory2 = [...prev.messageHistory, prev.currentMessage];
1870
+ newHistory2 = [...prev.messageHistory, messageToCommit];
1563
1871
  }
1564
1872
  }
1565
1873
  const toolMessageWithDuration = {
@@ -1581,7 +1889,13 @@ const App = ({
1581
1889
  }
1582
1890
  let newHistory = prev.messageHistory;
1583
1891
  if (prev.currentMessage && !shouldSkipMessage(prev.currentMessage)) {
1584
- newHistory = [...prev.messageHistory, prev.currentMessage];
1892
+ let msgToCommit = prev.currentMessage;
1893
+ if (bufferedContentRef.current.length > 0 && msgToCommit.role === "assistant" && msgToCommit.content.trim() === "") {
1894
+ msgToCommit = { ...msgToCommit, content: bufferedContentRef.current };
1895
+ bufferedContentRef.current = "";
1896
+ bufferedMessageIdRef.current = null;
1897
+ }
1898
+ newHistory = [...prev.messageHistory, msgToCommit];
1585
1899
  }
1586
1900
  return {
1587
1901
  ...prev,
@@ -1820,7 +2134,7 @@ const App = ({
1820
2134
  return prev;
1821
2135
  });
1822
2136
  });
1823
- }, [onMessage]);
2137
+ }, [appendStaticInfoMessage, onMessage, onShellInput, onSkillExecute, shouldSkipMessage, state]);
1824
2138
  React.useEffect(() => {
1825
2139
  onPlanModeChange((planMode) => {
1826
2140
  setState((prev) => ({
@@ -1969,29 +2283,52 @@ const App = ({
1969
2283
  onPlanApprovalRequest(async (plan) => {
1970
2284
  return new Promise((resolve) => {
1971
2285
  clearScreen();
1972
- setState((prev) => ({
1973
- ...prev,
1974
- screen: "plan-approval",
1975
- planApprovalRequest: {
1976
- plan,
1977
- resolve
1978
- },
1979
- isLoading: false
1980
- }));
2286
+ setState((prev) => {
2287
+ savedHistoryRef.current = [...prev.messageHistory];
2288
+ savedCurrentRef.current = prev.currentMessage;
2289
+ return {
2290
+ ...prev,
2291
+ screen: "plan-approval",
2292
+ planApprovalRequest: {
2293
+ plan,
2294
+ resolve
2295
+ },
2296
+ isLoading: false,
2297
+ // Clear messages so Ink's Static doesn't re-render chat above the plan screen
2298
+ messageHistory: [],
2299
+ currentMessage: null
2300
+ };
2301
+ });
1981
2302
  });
1982
2303
  });
1983
2304
  }, [clearScreen]);
1984
2305
  React.useEffect(() => {
1985
2306
  onPlanCreated((plan) => {
1986
2307
  try {
1987
- quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Plan created: ${plan.title} with ${plan.steps.length} tasks
2308
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [App] Plan created: ${plan.title} with ${plan.implementationSteps.length} steps
1988
2309
  `);
1989
2310
  } catch (e) {
1990
2311
  }
1991
2312
  });
1992
2313
  }, [onPlanCreated]);
1993
2314
  React.useEffect(() => {
1994
- onTaskCompleted((task, taskNumber, totalTasks, completionNote, taskDescription) => {
2315
+ onPlanQuestionRequest(async (question, options) => {
2316
+ return new Promise((resolve) => {
2317
+ setState((prev) => ({
2318
+ ...prev,
2319
+ screen: "plan-question",
2320
+ planQuestionRequest: {
2321
+ question,
2322
+ options,
2323
+ resolve
2324
+ },
2325
+ isLoading: false
2326
+ }));
2327
+ });
2328
+ });
2329
+ }, []);
2330
+ React.useEffect(() => {
2331
+ onTaskCompleted((stepNumber, stepDescription, completedCount, totalCount, completionNote, allSteps) => {
1995
2332
  setState((prev) => {
1996
2333
  const taskCompletedMessage = {
1997
2334
  id: `task-complete-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
@@ -2000,11 +2337,16 @@ const App = ({
2000
2337
  // We'll render this specially
2001
2338
  timestamp: /* @__PURE__ */ new Date(),
2002
2339
  taskCompletion: {
2003
- taskNumber,
2004
- totalTasks,
2005
- // Use the passed taskDescription if available (for subtasks), otherwise fall back to task.description
2006
- taskDescription: taskDescription || task.description,
2007
- completionNote
2340
+ completedStepNumber: stepNumber,
2341
+ completedStepDescription: stepDescription,
2342
+ completedCount,
2343
+ totalCount,
2344
+ completionNote,
2345
+ allSteps: allSteps?.map((s) => ({
2346
+ id: s.id,
2347
+ description: s.description,
2348
+ status: s.status
2349
+ }))
2008
2350
  }
2009
2351
  };
2010
2352
  const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage) ? [...prev.messageHistory, prev.currentMessage, taskCompletedMessage] : [...prev.messageHistory, taskCompletedMessage];
@@ -2016,6 +2358,34 @@ const App = ({
2016
2358
  });
2017
2359
  });
2018
2360
  }, [onTaskCompleted]);
2361
+ React.useEffect(() => {
2362
+ onTodoListUpdate((todos, completedCount, totalCount) => {
2363
+ setState((prev) => {
2364
+ const todoListMessage = {
2365
+ id: `todo-list-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
2366
+ role: "system",
2367
+ content: "",
2368
+ timestamp: /* @__PURE__ */ new Date(),
2369
+ todoList: {
2370
+ todos: todos.map((t) => ({
2371
+ id: t.id,
2372
+ content: t.content,
2373
+ status: t.status
2374
+ })),
2375
+ completedCount,
2376
+ totalCount
2377
+ }
2378
+ };
2379
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage) ? [...prev.messageHistory, prev.currentMessage, todoListMessage] : [...prev.messageHistory, todoListMessage];
2380
+ return {
2381
+ ...prev,
2382
+ messageHistory: newHistory,
2383
+ currentMessage: null,
2384
+ isAiWorking: true
2385
+ };
2386
+ });
2387
+ });
2388
+ }, [onTodoListUpdate]);
2019
2389
  React.useEffect(() => {
2020
2390
  onFileChangeSummary((data) => {
2021
2391
  setState((prev) => ({ ...prev, lastFileChangeSummary: data }));
@@ -2122,11 +2492,34 @@ const App = ({
2122
2492
  if (state.shellState?.isFocused) {
2123
2493
  return;
2124
2494
  }
2495
+ const isCtrlE = key.ctrl && input.toLowerCase() === "e" || input === "";
2496
+ if (key.meta && input.toLowerCase() === "e" || isCtrlE) {
2497
+ if (state.customTunnel && state.shellState?.isRunning) {
2498
+ flushTunnelOutput();
2499
+ clearScreen();
2500
+ onCustomTunnelStateChange(null);
2501
+ setState((prev) => ({
2502
+ ...prev,
2503
+ customTunnel: void 0,
2504
+ shellState: prev.shellState ? {
2505
+ ...prev.shellState,
2506
+ isFocused: true
2507
+ } : void 0
2508
+ }));
2509
+ return;
2510
+ }
2511
+ }
2125
2512
  if (key.ctrl && input === "f") {
2126
2513
  if (state.shellState && state.shellState.isRunning) {
2514
+ if (state.customTunnel) {
2515
+ flushTunnelOutput();
2516
+ onCustomTunnelStateChange(null);
2517
+ }
2127
2518
  clearScreen();
2128
2519
  setState((prev) => ({
2129
2520
  ...prev,
2521
+ customTunnel: void 0,
2522
+ // Always clear tunnel when toggling focus
2130
2523
  shellState: prev.shellState ? {
2131
2524
  ...prev.shellState,
2132
2525
  isFocused: !prev.shellState.isFocused
@@ -2155,7 +2548,7 @@ const App = ({
2155
2548
  }));
2156
2549
  return;
2157
2550
  }
2158
- if (state.screen === "mcp-add" || state.screen === "mcp-remove" || state.screen === "mcp-enable" || state.screen === "mcp-disable" || state.screen === "mcp-list") {
2551
+ if (state.screen === "mcp-remove" || state.screen === "mcp-enable" || state.screen === "mcp-disable" || state.screen === "mcp-list") {
2159
2552
  setState((prev) => ({
2160
2553
  ...prev,
2161
2554
  screen: "chat",
@@ -2163,8 +2556,12 @@ const App = ({
2163
2556
  }));
2164
2557
  return;
2165
2558
  }
2559
+ if (state.screen === "sub-agent-list" || state.screen === "sub-agent-view" || state.screen === "sub-agent-terminate" || state.screen === "skill-editor" || state.screen === "mcp-add" || state.screen === "rules-editor" || state.screen === "plan-question") {
2560
+ return;
2561
+ }
2166
2562
  if (state.isLoading || state.isAiWorking) {
2167
2563
  onCancelRequest();
2564
+ ignoreIncomingAIUpdatesRef.current = true;
2168
2565
  setState((prev) => ({
2169
2566
  ...prev,
2170
2567
  isLoading: false,
@@ -2191,6 +2588,7 @@ const App = ({
2191
2588
  }
2192
2589
  if (state.isLoading || state.isAiWorking) {
2193
2590
  onCancelRequest();
2591
+ ignoreIncomingAIUpdatesRef.current = true;
2194
2592
  setState((prev) => ({
2195
2593
  ...prev,
2196
2594
  isLoading: false,
@@ -2242,6 +2640,62 @@ const App = ({
2242
2640
  const wasAiWorkingBeforeSubmit = isAiWorkingRef.current;
2243
2641
  if (!trimmedValue) return;
2244
2642
  const isSlashCommand = trimmedValue.startsWith("/");
2643
+ if (customTunnelRef.current && shellRunningRef.current && !isSlashCommand) {
2644
+ const intent = detectIntent(trimmedValue);
2645
+ if (intent === "command") {
2646
+ if (clipboardFiles && clipboardFiles.length > 0) {
2647
+ appendStaticInfoMessage("File attachments are not available while custom tunnel mode is active.");
2648
+ return;
2649
+ }
2650
+ flushTunnelOutput();
2651
+ const tunnelInput = trimmedValue.endsWith("\n") || trimmedValue.endsWith("\r") ? trimmedValue : `${trimmedValue}\r`;
2652
+ try {
2653
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CustomTunnel] Sending chat input to shell: ${JSON.stringify(trimmedValue)}
2654
+ `);
2655
+ } catch (e) {
2656
+ }
2657
+ setState((prev) => {
2658
+ const userMessage = {
2659
+ id: `user-${Date.now()}`,
2660
+ role: "user",
2661
+ content: trimmedValue,
2662
+ timestamp: /* @__PURE__ */ new Date()
2663
+ };
2664
+ const messageHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage) ? [...prev.messageHistory, prev.currentMessage, userMessage] : [...prev.messageHistory, userMessage];
2665
+ return {
2666
+ ...prev,
2667
+ messageHistory,
2668
+ currentMessage: null,
2669
+ isLoading: false,
2670
+ isAiWorking: false,
2671
+ commandHistory: prev.commandHistory[prev.commandHistory.length - 1] === trimmedValue ? prev.commandHistory : [...prev.commandHistory, trimmedValue]
2672
+ };
2673
+ });
2674
+ onShellInput(tunnelInput);
2675
+ return;
2676
+ }
2677
+ }
2678
+ if (trimmedValue.startsWith("#")) {
2679
+ const parts = trimmedValue.slice(1).split(/\s+/);
2680
+ const skillName = parts[0];
2681
+ const params = parts.slice(1).join(" ");
2682
+ if (skillName) {
2683
+ const userMessage = {
2684
+ id: `user-${Date.now()}`,
2685
+ role: "user",
2686
+ content: trimmedValue,
2687
+ timestamp: /* @__PURE__ */ new Date()
2688
+ };
2689
+ setState((prev) => ({
2690
+ ...prev,
2691
+ messageHistory: [...prev.messageHistory, userMessage],
2692
+ isAiWorking: true
2693
+ }));
2694
+ await onSkillExecute(skillName, params);
2695
+ setState((prev) => ({ ...prev, isAiWorking: false }));
2696
+ return;
2697
+ }
2698
+ }
2245
2699
  const lowerValue = trimmedValue.toLowerCase();
2246
2700
  if (lowerValue === "/clear" || lowerValue === "clear" || lowerValue === "/cls" || lowerValue === "cls" || lowerValue === "/reset") {
2247
2701
  const height = process.stdout.rows || 24;
@@ -2505,7 +2959,7 @@ const App = ({
2505
2959
  isAiWorking: wasAiWorkingBeforeSubmit ? prev.isAiWorking : false
2506
2960
  }));
2507
2961
  }
2508
- }, [onMessage]);
2962
+ }, [appendStaticInfoMessage, flushTunnelOutput, onMessage, onShellInput, onSkillExecute, shouldSkipMessage]);
2509
2963
  const handleToggleAutoAccept = useCallback(() => {
2510
2964
  setState((prev) => ({
2511
2965
  ...prev,
@@ -2515,21 +2969,59 @@ const App = ({
2515
2969
  const handleInputValueChange = useCallback((value) => {
2516
2970
  preservedInputTextRef.current = value;
2517
2971
  }, []);
2972
+ const terminalColumns = process.stdout.columns || 120;
2973
+ const limitedHistory = useMemo(() => limitChatHistoryForDisplay(state.messageHistory, {
2974
+ enabled: state.limitChatHistory,
2975
+ width: terminalColumns
2976
+ }), [state.messageHistory, state.limitChatHistory, terminalColumns]);
2977
+ const displayedMessageHistory = useMemo(() => {
2978
+ if (limitedHistory.hiddenMessageCount === 0) {
2979
+ return limitedHistory.visibleMessages;
2980
+ }
2981
+ const hiddenCount = limitedHistory.hiddenMessageCount;
2982
+ const notice = {
2983
+ id: `__chat-history-limit__-${hiddenCount}`,
2984
+ role: "system",
2985
+ content: `\u2139\uFE0F Showing the most recent chat history only (${hiddenCount} older message${hiddenCount === 1 ? "" : "s"} hidden). Run \`/settings limit-chat-history off\` to show the full chat history in the UI.`,
2986
+ timestamp: /* @__PURE__ */ new Date(0)
2987
+ };
2988
+ return [notice, ...limitedHistory.visibleMessages];
2989
+ }, [limitedHistory]);
2990
+ React.useEffect(() => {
2991
+ const hiddenCount = limitedHistory.hiddenMessageCount;
2992
+ if (previousHiddenMessageCountRef.current === null) {
2993
+ previousHiddenMessageCountRef.current = hiddenCount;
2994
+ return;
2995
+ }
2996
+ if (hiddenCount === previousHiddenMessageCountRef.current) {
2997
+ return;
2998
+ }
2999
+ previousHiddenMessageCountRef.current = hiddenCount;
3000
+ if (state.screen !== "chat" || state.isInteractiveEditorMode || state.shellState?.isFocused) {
3001
+ return;
3002
+ }
3003
+ clearScreen();
3004
+ setState((prev) => ({
3005
+ ...prev,
3006
+ messageListKey: (prev.messageListKey ?? 0) + 1
3007
+ }));
3008
+ }, [limitedHistory.hiddenMessageCount, state.screen, state.isInteractiveEditorMode, state.shellState?.isFocused, clearScreen]);
2518
3009
  const sessionCommands = useMemo(() => {
2519
3010
  return state.messageHistory.filter((m) => m.role === "user").map((m) => m.content);
2520
3011
  }, [state.messageHistory]);
2521
3012
  if (state.isInteractiveEditorMode) {
2522
3013
  return /* @__PURE__ */ React.createElement(Box, null);
2523
3014
  }
2524
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, state.screen === "chat" && /* @__PURE__ */ React.createElement(React.Fragment, null, !state.shellState?.isFocused && /* @__PURE__ */ React.createElement(
3015
+ const themeColors = useMemo(() => deriveThemeColors(state.themeColor || DEFAULT_ACCENT_COLOR), [state.themeColor]);
3016
+ return /* @__PURE__ */ React.createElement(ThemeContext.Provider, { value: themeColors }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, state.screen === "chat" && /* @__PURE__ */ React.createElement(React.Fragment, null, !state.shellState?.isFocused && /* @__PURE__ */ React.createElement(
2525
3017
  MessageList,
2526
3018
  {
2527
3019
  key: `msglist-${state.messageListKey}`,
2528
- history: state.messageHistory,
3020
+ history: displayedMessageHistory,
2529
3021
  current: state.currentMessage,
2530
3022
  showBanner: true
2531
3023
  }
2532
- ), state.shellState && (getTerminalDimensions().shouldEnableStreaming || !state.shellState.isRunning) && /* @__PURE__ */ React.createElement(
3024
+ ), state.shellState && !state.customTunnel && (state.shellState.isFocused || getTerminalDimensions().shouldEnableStreaming || !state.shellState.isRunning) && /* @__PURE__ */ React.createElement(
2533
3025
  InteractiveShell,
2534
3026
  {
2535
3027
  command: state.shellState.command,
@@ -2622,90 +3114,128 @@ const App = ({
2622
3114
  `);
2623
3115
  }
2624
3116
  },
2625
- onWarpifySession: state.shellState?.isBackgroundTask ? void 0 : async () => {
2626
- const shellCommand = state.shellState?.command || "";
2627
- const currentOutput = state.shellState?.output || "";
2628
- const session = detectWarpifySession(shellCommand, currentOutput);
2629
- quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Warpify] Command: "${shellCommand}", Detected: ${JSON.stringify(session)}
3117
+ onWarpifySession: (() => {
3118
+ if (state.shellState?.isBackgroundTask) return void 0;
3119
+ const cmd = state.shellState?.command || "";
3120
+ const out = state.shellState?.output || "";
3121
+ const registered = getRegisteredTunnelCommands();
3122
+ const detected = detectWarpifySession(cmd, out, registered);
3123
+ if (detected.type === "none") return void 0;
3124
+ return async () => {
3125
+ const shellCommand = state.shellState?.command || "";
3126
+ const currentOutput = state.shellState?.output || "";
3127
+ const registeredTunnelCommands = getRegisteredTunnelCommands();
3128
+ const session = detectWarpifySession(shellCommand, currentOutput, registeredTunnelCommands);
3129
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Warpify] Command: "${shellCommand}", Detected: ${JSON.stringify(session)}
2630
3130
  `);
2631
- if (session.type === "none") {
2632
- setState((prev) => ({
2633
- ...prev,
2634
- connectionStatus: {
2635
- type: "ssh",
2636
- status: "error",
2637
- error: "No remote session detected. Enter an SSH/WSL/Docker session first."
2638
- }
2639
- }));
2640
- setTimeout(() => {
3131
+ if (session.type === "none") {
2641
3132
  setState((prev) => ({
2642
3133
  ...prev,
2643
- connectionStatus: void 0
3134
+ connectionStatus: {
3135
+ type: "ssh",
3136
+ status: "error",
3137
+ error: "No tunnelable session detected. Enter an SSH/WSL/Docker session first, or register a command with /settings add-tunnel-command."
3138
+ }
2644
3139
  }));
2645
- }, 3e3);
2646
- return;
2647
- }
2648
- const description = getSessionDescription(session);
2649
- quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Warpify] Warpifying: ${description}
2650
- `);
2651
- warpifyInProgressRef.current = true;
2652
- warpifyTerminatingCommandRef.current = shellCommand;
2653
- try {
2654
- onShellSignal("SIGTERM");
2655
- const historyMessages = [];
2656
- if (shellCommand) {
2657
- historyMessages.push({
2658
- id: `warpify-cmd-${Date.now()}`,
2659
- role: "user",
2660
- content: shellCommand,
2661
- timestamp: /* @__PURE__ */ new Date(),
2662
- isCommandMode: false
2663
- // Treat as a normal chat message for history
2664
- });
3140
+ setTimeout(() => {
3141
+ setState((prev) => ({
3142
+ ...prev,
3143
+ connectionStatus: void 0
3144
+ }));
3145
+ }, 3e3);
3146
+ return;
2665
3147
  }
2666
- if (currentOutput && currentOutput.trim().length > 0) {
2667
- historyMessages.push({
2668
- id: `warpify-out-${Date.now()}`,
2669
- role: "tool",
2670
- // Use 'tool' role to mimic command execution output
2671
- content: "",
2672
- timestamp: /* @__PURE__ */ new Date(),
2673
- toolExecution: {
2674
- toolName: "execute_command",
2675
- status: "completed",
2676
- result: currentOutput,
2677
- // This contains the PTY output including ANSI codes
2678
- arguments: {
2679
- command: shellCommand,
2680
- isPty: true
2681
- }
2682
- }
2683
- });
3148
+ if (session.type === "custom") {
3149
+ const matchedCommand = session.connectionString || shellCommand;
3150
+ const isAlreadyActive = state.customTunnel?.shellCommand === shellCommand;
3151
+ clearScreen();
3152
+ if (isAlreadyActive) {
3153
+ onCustomTunnelStateChange(null);
3154
+ setState((prev) => ({
3155
+ ...prev,
3156
+ customTunnel: void 0
3157
+ }));
3158
+ appendStaticInfoMessage(`Custom tunnel closed for \`${matchedCommand}\`. Chat input goes back to the AI.`);
3159
+ } else {
3160
+ tunnelOutputPositionRef.current = currentOutput.length;
3161
+ onCustomTunnelStateChange(matchedCommand);
3162
+ setState((prev) => ({
3163
+ ...prev,
3164
+ customTunnel: {
3165
+ matchedCommand,
3166
+ shellCommand
3167
+ },
3168
+ shellState: prev.shellState ? {
3169
+ ...prev.shellState,
3170
+ isFocused: false
3171
+ } : void 0
3172
+ }));
3173
+ appendStaticInfoMessage(`Custom tunnel active for \`${matchedCommand}\`. Chat messages are now sent to the process. Press ${process.platform === "darwin" ? "Cmd+E" : "Alt+E"} from chat to return to shell focus mode.`);
3174
+ }
3175
+ return;
2684
3176
  }
2685
- setState((prev) => ({
2686
- ...prev,
2687
- messageHistory: [...prev.messageHistory, ...historyMessages],
2688
- shellState: void 0,
2689
- currentMessage: null,
2690
- isAiWorking: false
2691
- }));
2692
- clearScreen();
2693
- const success = await onWarpifySession(
2694
- shellCommand,
2695
- session.type,
2696
- session.connectionString
2697
- );
2698
- if (!success) {
2699
- quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Warpify] Failed to establish ssh2 connection
3177
+ const description = getSessionDescription(session);
3178
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Warpify] Warpifying: ${description}
2700
3179
  `);
3180
+ warpifyInProgressRef.current = true;
3181
+ warpifyTerminatingCommandRef.current = shellCommand;
3182
+ try {
3183
+ onShellSignal("SIGTERM");
3184
+ const historyMessages = [];
3185
+ if (shellCommand) {
3186
+ historyMessages.push({
3187
+ id: `warpify-cmd-${Date.now()}`,
3188
+ role: "user",
3189
+ content: shellCommand,
3190
+ timestamp: /* @__PURE__ */ new Date(),
3191
+ isCommandMode: false
3192
+ // Treat as a normal chat message for history
3193
+ });
3194
+ }
3195
+ if (currentOutput && currentOutput.trim().length > 0) {
3196
+ historyMessages.push({
3197
+ id: `warpify-out-${Date.now()}`,
3198
+ role: "tool",
3199
+ // Use 'tool' role to mimic command execution output
3200
+ content: "",
3201
+ timestamp: /* @__PURE__ */ new Date(),
3202
+ toolExecution: {
3203
+ toolName: "execute_command",
3204
+ status: "completed",
3205
+ result: currentOutput,
3206
+ // This contains the PTY output including ANSI codes
3207
+ arguments: {
3208
+ command: shellCommand,
3209
+ isPty: true
3210
+ }
3211
+ }
3212
+ });
3213
+ }
3214
+ setState((prev) => ({
3215
+ ...prev,
3216
+ messageHistory: [...prev.messageHistory, ...historyMessages],
3217
+ shellState: void 0,
3218
+ currentMessage: null,
3219
+ isAiWorking: false
3220
+ }));
3221
+ clearScreen();
3222
+ const success = await onWarpifySession(
3223
+ shellCommand,
3224
+ session.type,
3225
+ session.connectionString
3226
+ );
3227
+ if (!success) {
3228
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Warpify] Failed to establish ssh2 connection
3229
+ `);
3230
+ }
3231
+ } finally {
3232
+ setTimeout(() => {
3233
+ warpifyInProgressRef.current = false;
3234
+ warpifyTerminatingCommandRef.current = null;
3235
+ }, 400);
2701
3236
  }
2702
- } finally {
2703
- setTimeout(() => {
2704
- warpifyInProgressRef.current = false;
2705
- warpifyTerminatingCommandRef.current = null;
2706
- }, 400);
2707
- }
2708
- }
3237
+ };
3238
+ })()
2709
3239
  }
2710
3240
  ), state.shellState?.isAgentControlled && state.shellState?.isFocused && state.shellState?.isRunning && /* @__PURE__ */ React.createElement(
2711
3241
  MonitorModeAIPanel,
@@ -2724,7 +3254,22 @@ const App = ({
2724
3254
  maxHeight: Math.floor((process.stdout.rows || 24) / 2) - 5,
2725
3255
  isActive: true
2726
3256
  }
2727
- ), (state.isAiWorking && !state.shellState && !state.approvalRequest && !(state.currentMessage?.toolExecution?.status === "executing") || state.lastFileChangeSummary && state.lastFileChangeSummary.filesChanged > 0) && /* @__PURE__ */ React.createElement(Box, { marginBottom: 1, flexDirection: "row", justifyContent: "space-between", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { paddingLeft: 1 }, state.isAiWorking && !state.shellState && !state.approvalRequest && !(state.currentMessage?.toolExecution?.status === "executing") && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(LoadingIndicator, { key: "loading-indicator" }), /* @__PURE__ */ React.createElement(AgentTimer, { key: "agent-timer" }))), state.lastFileChangeSummary && state.lastFileChangeSummary.filesChanged > 0 && /* @__PURE__ */ React.createElement(Box, { paddingRight: 1, flexDirection: "row" }, /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, state.lastFileChangeSummary.filesChanged, " file", state.lastFileChangeSummary.filesChanged !== 1 ? "s" : "", " changed "), state.lastFileChangeSummary.insertions > 0 && /* @__PURE__ */ React.createElement(Text, { color: "green" }, "+", state.lastFileChangeSummary.insertions), state.lastFileChangeSummary.insertions > 0 && state.lastFileChangeSummary.deletions > 0 && /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " "), state.lastFileChangeSummary.deletions > 0 && /* @__PURE__ */ React.createElement(Text, { color: "red" }, "-", state.lastFileChangeSummary.deletions))), state.interruptQueue && state.interruptQueue.length > 0 && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, state.interruptQueue.map((msg, i) => /* @__PURE__ */ React.createElement(Text, { key: `interrupt-${i}`, color: "gray", dimColor: true }, "[Queued Interrupt ", i + 1, "/", state.interruptQueue.length, "] ", msg))), state.commandQueue && state.commandQueue.length > 0 && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, state.commandQueue.map((command, i) => /* @__PURE__ */ React.createElement(Text, { key: `queued-command-${i}`, color: "#00cc66" }, "[Queued Command ", i + 1, "/", state.commandQueue.length, "] $ ", command))), !state.shellState?.isFocused && (state.passwordRequest ? /* @__PURE__ */ React.createElement(
3257
+ ), state.shellState && !state.customTunnel && !state.shellState.isFocused && !getTerminalDimensions().shouldEnableStreaming && state.shellState.isRunning && (process.stdout.rows || 24) >= 15 && /* @__PURE__ */ React.createElement(
3258
+ CompactShellPreview,
3259
+ {
3260
+ command: state.shellState.command,
3261
+ output: state.shellState.output,
3262
+ isRunning: state.shellState.isRunning,
3263
+ remoteContext: state.shellState.remoteContext
3264
+ }
3265
+ ), (() => {
3266
+ const dims = getTerminalDimensions();
3267
+ const isCompact = !dims.shouldEnableStreaming;
3268
+ const showLoading = state.isAiWorking && !state.shellState && !state.approvalRequest && !(state.currentMessage?.toolExecution?.status === "executing");
3269
+ const showFileChanges = !isCompact && state.lastFileChangeSummary && state.lastFileChangeSummary.filesChanged > 0;
3270
+ if ((!showLoading || isCompact) && !showFileChanges) return null;
3271
+ return /* @__PURE__ */ React.createElement(Box, { marginBottom: 1, flexDirection: "row", justifyContent: "space-between", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { paddingLeft: 1, flexDirection: "row" }, showLoading && !isCompact && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(LoadingIndicator, { key: "loading-indicator" }), /* @__PURE__ */ React.createElement(AgentTimer, { key: "agent-timer" }))), showFileChanges && /* @__PURE__ */ React.createElement(Box, { paddingRight: 1, flexDirection: "row", flexShrink: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, state.lastFileChangeSummary.filesChanged, " file", state.lastFileChangeSummary.filesChanged !== 1 ? "s" : "", " changed "), state.lastFileChangeSummary.insertions > 0 && /* @__PURE__ */ React.createElement(Text, { color: "green" }, "+", state.lastFileChangeSummary.insertions), state.lastFileChangeSummary.insertions > 0 && state.lastFileChangeSummary.deletions > 0 && /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " "), state.lastFileChangeSummary.deletions > 0 && /* @__PURE__ */ React.createElement(Text, { color: "red" }, "-", state.lastFileChangeSummary.deletions)));
3272
+ })(), state.interruptQueue && state.interruptQueue.length > 0 && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, state.interruptQueue.map((msg, i) => /* @__PURE__ */ React.createElement(Text, { key: `interrupt-${i}`, color: "gray", dimColor: true }, "[Queued Interrupt ", i + 1, "/", state.interruptQueue.length, "] ", msg))), state.commandQueue && state.commandQueue.length > 0 && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, state.commandQueue.map((command, i) => /* @__PURE__ */ React.createElement(Text, { key: `queued-command-${i}`, color: "#00cc66" }, "[Queued Command ", i + 1, "/", state.commandQueue.length, "] $ ", command))), !state.shellState?.isFocused && (state.passwordRequest ? /* @__PURE__ */ React.createElement(
2728
3273
  PasswordPrompt,
2729
3274
  {
2730
3275
  message: state.passwordRequest.message,
@@ -2762,6 +3307,7 @@ const App = ({
2762
3307
  preservedInputTextRef.current = "";
2763
3308
  handleSubmit(value, clipboardImages);
2764
3309
  },
3310
+ placeholder: state.customTunnel ? `Send input to ${state.customTunnel.matchedCommand}` : "Ask anything ...",
2765
3311
  autoAcceptMode: state.autoAcceptMode,
2766
3312
  model: state.currentModel,
2767
3313
  planMode: state.planMode,
@@ -2771,6 +3317,7 @@ const App = ({
2771
3317
  commandHistory: state.commandHistory,
2772
3318
  onToggleAutoAccept: handleToggleAutoAccept,
2773
3319
  onToggleCommandMode,
3320
+ onTogglePlanMode,
2774
3321
  onToggleBackgroundMode,
2775
3322
  isActive: true,
2776
3323
  subshellContext: state.subshellContext,
@@ -2779,6 +3326,8 @@ const App = ({
2779
3326
  maxTokens: state.maxTokens,
2780
3327
  contextLimitReached: state.contextLimitReached,
2781
3328
  isShellRunning: state.shellState?.isRunning,
3329
+ allowSubmitWhileShellRunning: !!state.customTunnel,
3330
+ customTunnelCommand: state.customTunnel?.matchedCommand,
2782
3331
  backgroundTaskCount: state.backgroundTaskCount,
2783
3332
  subAgentCount: state.subAgentCount,
2784
3333
  initialValue: preservedInputTextRef.current,
@@ -2788,13 +3337,15 @@ const App = ({
2788
3337
  },
2789
3338
  sessionQuotaExhausted: state.sessionQuotaExhausted,
2790
3339
  sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining,
2791
- aiAutoSuggestEnabled: state.aiAutoSuggest,
3340
+ aiAutoSuggestEnabled: state.customTunnel ? false : state.aiAutoSuggest,
2792
3341
  sessionCommands,
2793
3342
  getCheckpoints,
2794
3343
  onSetInputSetup,
2795
- gitDiffStats: state.gitDiffStats
3344
+ gitDiffStats: state.gitDiffStats,
3345
+ isAiWorking: state.isAiWorking,
3346
+ lastFileChangeSummary: state.lastFileChangeSummary
2796
3347
  }
2797
- )), state.showExitWarning && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#ffaa00", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit")), !isConnected && /* @__PURE__ */ React.createElement(Box, { marginTop: 0, marginLeft: state.showExitWarning ? 2 : 0 }, /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F No internet connection"))), state.screen === "approval" && state.approvalRequest && /* @__PURE__ */ React.createElement(
3348
+ )), state.showExitWarning && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#ffaa00", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit")), !isConnected && /* @__PURE__ */ React.createElement(Box, { marginTop: 0, marginLeft: state.showExitWarning ? 2 : 0 }, /* @__PURE__ */ React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F No internet connection"))), state.screen !== "chat" && /* @__PURE__ */ React.createElement(ScreenContainer, null, state.screen === "approval" && state.approvalRequest && /* @__PURE__ */ React.createElement(
2798
3349
  ApprovalSection,
2799
3350
  {
2800
3351
  key: `approval-${state.approvalRequest.message}`,
@@ -2806,7 +3357,43 @@ const App = ({
2806
3357
  }
2807
3358
  }
2808
3359
  }
2809
- ), state.screen === "help" && /* @__PURE__ */ React.createElement(KeyboardHelp, { onClose: () => setState((prev) => ({ ...prev, screen: "chat" })) }), state.screen === "picker" && state.pickerOptions && /* @__PURE__ */ React.createElement(
3360
+ ), state.screen === "help" && /* @__PURE__ */ React.createElement(KeyboardHelp, { onClose: () => setState((prev) => ({ ...prev, screen: "chat" })) }), state.screen === "picker" && state.pickerOptions && (state.pickerOptions.type === "model" && state.pickerOptions.modelConfigs ? /* @__PURE__ */ React.createElement(
3361
+ ModelPicker,
3362
+ {
3363
+ models: state.pickerOptions.modelConfigs,
3364
+ currentModelName: state.pickerOptions.currentModelName || "",
3365
+ onCancel: () => {
3366
+ setState((prev) => ({ ...prev, screen: "chat", isLoading: false, isAiWorking: false }));
3367
+ },
3368
+ onSelect: async (value) => {
3369
+ setState((prev) => ({ ...prev, screen: "chat", isLoading: true }));
3370
+ try {
3371
+ await onPickerSelection(value, state.pickerOptions.type);
3372
+ setState((prev) => ({
3373
+ ...prev,
3374
+ isLoading: false,
3375
+ isAiWorking: false
3376
+ }));
3377
+ } catch (error) {
3378
+ setState((prev) => {
3379
+ const errorMessage = {
3380
+ id: `system-error-${Date.now()}`,
3381
+ role: "system",
3382
+ content: `Error: ${error.message || "Unknown error occurred"}`,
3383
+ timestamp: /* @__PURE__ */ new Date()
3384
+ };
3385
+ return {
3386
+ ...prev,
3387
+ messageHistory: [...prev.messageHistory, errorMessage],
3388
+ currentMessage: null,
3389
+ isLoading: false,
3390
+ isAiWorking: false
3391
+ };
3392
+ });
3393
+ }
3394
+ }
3395
+ }
3396
+ ) : /* @__PURE__ */ React.createElement(
2810
3397
  SelectPrompt,
2811
3398
  {
2812
3399
  message: state.pickerOptions.message,
@@ -2825,7 +3412,7 @@ const App = ({
2825
3412
  const errorMessage = {
2826
3413
  id: `system-error-${Date.now()}`,
2827
3414
  role: "system",
2828
- content: `\u274C Error: ${error.message || "Unknown error occurred"}`,
3415
+ content: `Error: ${error.message || "Unknown error occurred"}`,
2829
3416
  timestamp: /* @__PURE__ */ new Date()
2830
3417
  };
2831
3418
  return {
@@ -2839,7 +3426,7 @@ const App = ({
2839
3426
  }
2840
3427
  }
2841
3428
  }
2842
- ), state.screen === "chat-picker" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "\u{1F4DA} Resume a Previous Chat"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
3429
+ )), state.screen === "chat-picker" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: themeColors.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: themeColors.accent, bold: true }, "\u{1F4DA} Resume a Previous Chat"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2843
3430
  CircularSelectInput,
2844
3431
  {
2845
3432
  limit: listLimit,
@@ -2887,7 +3474,7 @@ const App = ({
2887
3474
  }
2888
3475
  }
2889
3476
  }
2890
- )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))), state.screen === "chat-delete-picker" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "\u{1F5D1}\uFE0F Select a Chat to Delete"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
3477
+ )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))), state.screen === "chat-delete-picker" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: themeColors.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: themeColors.accent, bold: true }, "\u{1F5D1}\uFE0F Select a Chat to Delete"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2891
3478
  CircularSelectInput,
2892
3479
  {
2893
3480
  key: `chat-delete-picker-${state.chatDeletePickerSession}`,
@@ -2933,7 +3520,7 @@ const App = ({
2933
3520
  }
2934
3521
  }
2935
3522
  }
2936
- )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))), state.screen === "chat-list-view" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "\u{1F4DA} Saved Chats (", state.chatPickerChats.length, ")"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
3523
+ )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))), state.screen === "chat-list-view" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: themeColors.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: themeColors.accent, bold: true }, "\u{1F4DA} Saved Chats (", state.chatPickerChats.length, ")"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2937
3524
  CircularSelectInput,
2938
3525
  {
2939
3526
  limit: listLimit,
@@ -2957,7 +3544,7 @@ const App = ({
2957
3544
  setState((prev) => ({ ...prev, screen: "chat" }));
2958
3545
  }
2959
3546
  }
2960
- )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))), state.screen === "chat-rename-picker" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Select a Chat to Rename"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
3547
+ )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))), state.screen === "chat-rename-picker" && state.chatPickerChats && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: themeColors.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: themeColors.accent, bold: true }, "\u270F\uFE0F Select a Chat to Rename"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
2961
3548
  CircularSelectInput,
2962
3549
  {
2963
3550
  limit: listLimit,
@@ -3029,18 +3616,23 @@ const App = ({
3029
3616
  timestamp: /* @__PURE__ */ new Date(),
3030
3617
  planAccepted: {
3031
3618
  planTitle: plan?.title,
3032
- totalTasks: plan?.steps?.length,
3033
- tasks: plan?.steps?.map((step) => ({
3619
+ totalSteps: plan?.implementationSteps?.length,
3620
+ steps: plan?.implementationSteps?.map((step) => ({
3621
+ id: step.id,
3034
3622
  description: step.description,
3035
- subtasks: step.subtasks?.map((st) => ({ description: st.description }))
3623
+ status: step.status
3036
3624
  }))
3037
3625
  }
3038
3626
  };
3627
+ const restoredHistory = savedHistoryRef.current || [];
3628
+ clearScreen();
3039
3629
  setState((prev) => ({
3040
3630
  ...prev,
3041
3631
  screen: "chat",
3042
3632
  planApprovalRequest: void 0,
3043
- messageHistory: [...prev.messageHistory, planAcceptedMessage],
3633
+ messageHistory: [...restoredHistory, planAcceptedMessage],
3634
+ currentMessage: savedCurrentRef.current,
3635
+ messageListKey: prev.messageListKey + 1,
3044
3636
  isLoading: true,
3045
3637
  isAiWorking: true
3046
3638
  }));
@@ -3050,10 +3642,77 @@ const App = ({
3050
3642
  if (resolve) {
3051
3643
  resolve(false);
3052
3644
  }
3645
+ const restoredHistory = savedHistoryRef.current || [];
3646
+ clearScreen();
3053
3647
  setState((prev) => ({
3054
3648
  ...prev,
3055
3649
  screen: "chat",
3056
- planApprovalRequest: void 0
3650
+ planApprovalRequest: void 0,
3651
+ messageHistory: restoredHistory,
3652
+ currentMessage: savedCurrentRef.current,
3653
+ messageListKey: prev.messageListKey + 1
3654
+ }));
3655
+ }
3656
+ }
3657
+ ), state.screen === "plan-question" && state.planQuestionRequest && /* @__PURE__ */ React.createElement(
3658
+ PlanQuestionScreen,
3659
+ {
3660
+ question: state.planQuestionRequest.question,
3661
+ options: state.planQuestionRequest.options,
3662
+ onAnswer: (answer) => {
3663
+ const resolve = state.planQuestionRequest?.resolve;
3664
+ const question = state.planQuestionRequest?.question || "";
3665
+ const options = state.planQuestionRequest?.options || [];
3666
+ if (resolve) {
3667
+ resolve(answer);
3668
+ }
3669
+ const questionMessage = {
3670
+ id: `plan-question-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
3671
+ role: "system",
3672
+ content: "",
3673
+ timestamp: /* @__PURE__ */ new Date(),
3674
+ planQuestion: {
3675
+ question,
3676
+ options,
3677
+ userAnswer: answer,
3678
+ wasSkipped: false
3679
+ }
3680
+ };
3681
+ setState((prev) => ({
3682
+ ...prev,
3683
+ screen: "chat",
3684
+ planQuestionRequest: void 0,
3685
+ messageHistory: [...prev.messageHistory, questionMessage],
3686
+ isLoading: true,
3687
+ isAiWorking: true
3688
+ }));
3689
+ },
3690
+ onSkip: () => {
3691
+ const resolve = state.planQuestionRequest?.resolve;
3692
+ const question = state.planQuestionRequest?.question || "";
3693
+ const options = state.planQuestionRequest?.options || [];
3694
+ if (resolve) {
3695
+ resolve("[User skipped this question. Proceed with the most appropriate and optimal approach as you see fit.]");
3696
+ }
3697
+ const questionMessage = {
3698
+ id: `plan-question-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
3699
+ role: "system",
3700
+ content: "",
3701
+ timestamp: /* @__PURE__ */ new Date(),
3702
+ planQuestion: {
3703
+ question,
3704
+ options,
3705
+ userAnswer: "",
3706
+ wasSkipped: true
3707
+ }
3708
+ };
3709
+ setState((prev) => ({
3710
+ ...prev,
3711
+ screen: "chat",
3712
+ planQuestionRequest: void 0,
3713
+ messageHistory: [...prev.messageHistory, questionMessage],
3714
+ isLoading: true,
3715
+ isAiWorking: true
3057
3716
  }));
3058
3717
  }
3059
3718
  }
@@ -3334,7 +3993,142 @@ Use it in prompts with: @rules:${savedName}`,
3334
3993
  }));
3335
3994
  }
3336
3995
  }
3337
- ));
3996
+ ), state.screen === "skill-editor" && state.skillEditorRequest && /* @__PURE__ */ React.createElement(
3997
+ SkillCreatorScreen,
3998
+ {
3999
+ mode: state.skillEditorRequest.mode,
4000
+ initialSkill: state.skillEditorRequest.initialSkill,
4001
+ onSave: (skill, previousName) => {
4002
+ const result = onSkillSave(skill, previousName);
4003
+ if (!result.success) {
4004
+ return result;
4005
+ }
4006
+ const savedName = result.savedName || skill.name;
4007
+ const successMessage = {
4008
+ id: `skill-saved-${Date.now()}`,
4009
+ role: "system",
4010
+ content: `\u2705 Skill "${savedName}" saved successfully.
4011
+
4012
+ Invoke it with: #${savedName} <parameters>`,
4013
+ timestamp: /* @__PURE__ */ new Date()
4014
+ };
4015
+ setState((prev) => ({
4016
+ ...prev,
4017
+ screen: "chat",
4018
+ skillEditorRequest: void 0,
4019
+ messageHistory: [...prev.messageHistory, successMessage]
4020
+ }));
4021
+ return result;
4022
+ },
4023
+ onCancel: () => {
4024
+ setState((prev) => ({
4025
+ ...prev,
4026
+ screen: "chat",
4027
+ skillEditorRequest: void 0
4028
+ }));
4029
+ }
4030
+ }
4031
+ ), state.screen === "sub-agent-list" && state.subAgentListData && /* @__PURE__ */ React.createElement(
4032
+ SubAgentListScreen,
4033
+ {
4034
+ agents: state.subAgentListData,
4035
+ onSelect: (agentId) => {
4036
+ clearScreen();
4037
+ setState((prev) => ({ ...prev, screen: "sub-agent-view", viewingSubAgentId: agentId }));
4038
+ },
4039
+ onCancel: () => {
4040
+ clearScreen();
4041
+ setState((prev) => ({
4042
+ ...prev,
4043
+ screen: "chat",
4044
+ subAgentListData: void 0,
4045
+ messageListKey: (prev.messageListKey ?? 0) + 1
4046
+ }));
4047
+ }
4048
+ }
4049
+ ), state.screen === "sub-agent-view" && state.viewingSubAgentId && /* @__PURE__ */ React.createElement(
4050
+ SubAgentViewScreen,
4051
+ {
4052
+ agentId: state.viewingSubAgentId,
4053
+ onBack: () => {
4054
+ clearScreen();
4055
+ const allAgents = SubAgentManager.getAllSubAgents();
4056
+ if (allAgents.length > 0) {
4057
+ const agentList = allAgents.map((a) => ({
4058
+ id: a.id,
4059
+ status: a.status,
4060
+ prompt: a.prompt,
4061
+ model: a.model,
4062
+ startTime: a.startTime,
4063
+ turnCount: a.turnCount,
4064
+ fileOpsCount: a.fileOperations.length
4065
+ }));
4066
+ setState((prev) => ({ ...prev, screen: "sub-agent-list", subAgentListData: agentList, viewingSubAgentId: void 0 }));
4067
+ } else {
4068
+ setState((prev) => ({
4069
+ ...prev,
4070
+ screen: "chat",
4071
+ viewingSubAgentId: void 0,
4072
+ subAgentListData: void 0,
4073
+ messageListKey: (prev.messageListKey ?? 0) + 1
4074
+ }));
4075
+ }
4076
+ },
4077
+ onTerminate: (agentId) => {
4078
+ SubAgentManager.terminateSubAgent(agentId);
4079
+ },
4080
+ onAutoReturn: () => {
4081
+ clearScreen();
4082
+ setState((prev) => ({
4083
+ ...prev,
4084
+ screen: "chat",
4085
+ viewingSubAgentId: void 0,
4086
+ subAgentListData: void 0,
4087
+ messageListKey: (prev.messageListKey ?? 0) + 1
4088
+ }));
4089
+ }
4090
+ }
4091
+ ), state.screen === "sub-agent-terminate" && state.subAgentListData && /* @__PURE__ */ React.createElement(
4092
+ SubAgentListScreen,
4093
+ {
4094
+ agents: state.subAgentListData,
4095
+ onSelect: (agentId) => {
4096
+ SubAgentManager.terminateSubAgent(agentId);
4097
+ const runningAgents = SubAgentManager.getRunningSubAgents();
4098
+ if (runningAgents.length === 0) {
4099
+ clearScreen();
4100
+ setState((prev) => ({
4101
+ ...prev,
4102
+ screen: "chat",
4103
+ subAgentListData: void 0,
4104
+ messageListKey: (prev.messageListKey ?? 0) + 1
4105
+ }));
4106
+ } else {
4107
+ setState((prev) => ({
4108
+ ...prev,
4109
+ subAgentListData: runningAgents.map((a) => ({
4110
+ id: a.id,
4111
+ status: a.status,
4112
+ prompt: a.prompt,
4113
+ model: a.model,
4114
+ startTime: a.startTime,
4115
+ turnCount: a.turnCount,
4116
+ fileOpsCount: a.fileOperations.length
4117
+ }))
4118
+ }));
4119
+ }
4120
+ },
4121
+ onCancel: () => {
4122
+ clearScreen();
4123
+ setState((prev) => ({
4124
+ ...prev,
4125
+ screen: "chat",
4126
+ subAgentListData: void 0,
4127
+ messageListKey: (prev.messageListKey ?? 0) + 1
4128
+ }));
4129
+ }
4130
+ }
4131
+ ))));
3338
4132
  };
3339
4133
  export {
3340
4134
  App