centaurus-cli 3.1.3 → 3.1.5

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 (138) hide show
  1. package/dist/cli-adapter.js +685 -153
  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 +4 -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/api-client.js.map +1 -1
  16. package/dist/services/background-task-manager.js +59 -0
  17. package/dist/services/background-task-manager.js.map +1 -1
  18. package/dist/services/local-chat-storage.js +2 -0
  19. package/dist/services/local-chat-storage.js.map +1 -1
  20. package/dist/services/skill-storage.js +141 -0
  21. package/dist/services/skill-storage.js.map +1 -0
  22. package/dist/services/sub-agent-manager.js +49 -8
  23. package/dist/services/sub-agent-manager.js.map +1 -1
  24. package/dist/services/warpify-detector.js +17 -5
  25. package/dist/services/warpify-detector.js.map +1 -1
  26. package/dist/tools/background-command.js +5 -2
  27. package/dist/tools/background-command.js.map +1 -1
  28. package/dist/tools/command.js +367 -109
  29. package/dist/tools/command.js.map +1 -1
  30. package/dist/tools/file-ops.js +23 -6
  31. package/dist/tools/file-ops.js.map +1 -1
  32. package/dist/tools/plan-mode.js +184 -336
  33. package/dist/tools/plan-mode.js.map +1 -1
  34. package/dist/tools/sub-agent.js +24 -5
  35. package/dist/tools/sub-agent.js.map +1 -1
  36. package/dist/tools/todo-list.js +157 -0
  37. package/dist/tools/todo-list.js.map +1 -0
  38. package/dist/types/skill.js +30 -0
  39. package/dist/types/skill.js.map +1 -0
  40. package/dist/ui/components/App.js +956 -162
  41. package/dist/ui/components/App.js.map +1 -1
  42. package/dist/ui/components/AuthScreen.js +3 -1
  43. package/dist/ui/components/AuthScreen.js.map +1 -1
  44. package/dist/ui/components/AuthWelcomeScreen.js +3 -1
  45. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  46. package/dist/ui/components/CodeBlock.js +3 -1
  47. package/dist/ui/components/CodeBlock.js.map +1 -1
  48. package/dist/ui/components/CompactShellPreview.js +44 -0
  49. package/dist/ui/components/CompactShellPreview.js.map +1 -0
  50. package/dist/ui/components/ConfigViewer.js +3 -1
  51. package/dist/ui/components/ConfigViewer.js.map +1 -1
  52. package/dist/ui/components/ConfirmPrompt.js +3 -1
  53. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  54. package/dist/ui/components/ConnectionStatusMessage.js +3 -1
  55. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  56. package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
  57. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  58. package/dist/ui/components/DiffViewer.js +6 -3
  59. package/dist/ui/components/DiffViewer.js.map +1 -1
  60. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  61. package/dist/ui/components/FileTagAutocomplete.js +4 -2
  62. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  63. package/dist/ui/components/InputBox.js +243 -40
  64. package/dist/ui/components/InputBox.js.map +1 -1
  65. package/dist/ui/components/InteractiveShell.js +5 -3
  66. package/dist/ui/components/InteractiveShell.js.map +1 -1
  67. package/dist/ui/components/KeyboardHelp.js +4 -1
  68. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  69. package/dist/ui/components/LoadingIndicator.js +3 -1
  70. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  71. package/dist/ui/components/MCPAddScreen.js +63 -13
  72. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  73. package/dist/ui/components/MarkdownRenderer.js +3 -1
  74. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  75. package/dist/ui/components/MessageDisplay.js +9 -7
  76. package/dist/ui/components/MessageDisplay.js.map +1 -1
  77. package/dist/ui/components/ModelPicker.js +170 -0
  78. package/dist/ui/components/ModelPicker.js.map +1 -0
  79. package/dist/ui/components/MonitorModeAIPanel.js +3 -1
  80. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  81. package/dist/ui/components/PlanAcceptedMessage.js +12 -6
  82. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  83. package/dist/ui/components/PlanQuestionMessage.js +37 -0
  84. package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
  85. package/dist/ui/components/PlanQuestionScreen.js +138 -0
  86. package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
  87. package/dist/ui/components/PlanReviewScreen.js +7 -9
  88. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  89. package/dist/ui/components/RulesEditorScreen.js +65 -28
  90. package/dist/ui/components/RulesEditorScreen.js.map +1 -1
  91. package/dist/ui/components/SelectPrompt.js +3 -1
  92. package/dist/ui/components/SelectPrompt.js.map +1 -1
  93. package/dist/ui/components/SkillCreatorScreen.js +217 -0
  94. package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
  95. package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
  96. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  97. package/dist/ui/components/StatusBar.js +4 -2
  98. package/dist/ui/components/StatusBar.js.map +1 -1
  99. package/dist/ui/components/StreamingMessageDisplay.js +5 -3
  100. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  101. package/dist/ui/components/SubAgentListScreen.js +65 -0
  102. package/dist/ui/components/SubAgentListScreen.js.map +1 -0
  103. package/dist/ui/components/SubAgentViewScreen.js +123 -0
  104. package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
  105. package/dist/ui/components/TaskCompletedMessage.js +40 -8
  106. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  107. package/dist/ui/components/TaskProgressIndicator.js +6 -4
  108. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  109. package/dist/ui/components/TextEditor.js +297 -0
  110. package/dist/ui/components/TextEditor.js.map +1 -0
  111. package/dist/ui/components/TodoListMessage.js +59 -0
  112. package/dist/ui/components/TodoListMessage.js.map +1 -0
  113. package/dist/ui/components/ToolExecutionMessage.js +134 -84
  114. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  115. package/dist/ui/components/ToolExecutionStatus.js +3 -1
  116. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  117. package/dist/ui/components/WelcomeBanner.js +33 -33
  118. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  119. package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
  120. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  121. package/dist/ui/theme.js +97 -0
  122. package/dist/ui/theme.js.map +1 -0
  123. package/dist/ui/utils/chat-history-limit.js +247 -0
  124. package/dist/ui/utils/chat-history-limit.js.map +1 -0
  125. package/dist/utils/chat-formatter.js +22 -9
  126. package/dist/utils/chat-formatter.js.map +1 -1
  127. package/dist/utils/input-classifier.js +11 -1
  128. package/dist/utils/input-classifier.js.map +1 -1
  129. package/dist/utils/output-truncation.js +175 -0
  130. package/dist/utils/output-truncation.js.map +1 -0
  131. package/dist/utils/rule-reference-resolver.js +3 -3
  132. package/dist/utils/rule-reference-resolver.js.map +1 -1
  133. package/dist/utils/tunnel-commands-manager.js +134 -0
  134. package/dist/utils/tunnel-commands-manager.js.map +1 -0
  135. package/package.json +91 -90
  136. package/postinstall.js +4 -11
  137. package/dist/ui/components/MultiLineInput.js +0 -255
  138. package/dist/ui/components/MultiLineInput.js.map +0 -1
@@ -6,6 +6,7 @@ import { encodeKeyToAnsi } from "../../utils/ansi-encoder.js";
6
6
  import { processTerminalOutput } from "../../utils/terminal-output.js";
7
7
  import { TERMINAL_HEIGHT_CONSTANTS } from "../../hooks/useTerminalDimensions.js";
8
8
  import { sanitizeUnicode } from "../../utils/unicode-sanitizer.js";
9
+ import { useTheme } from "../theme.js";
9
10
  function formatCwdWithContext(cwd, remoteContext) {
10
11
  if (remoteContext && cwd) {
11
12
  return `${remoteContext}:${cwd.replace(/\\/g, "/")}`;
@@ -36,6 +37,7 @@ const InteractiveShell = React.memo(({
36
37
  onWarpifySession
37
38
  }) => {
38
39
  const isMac = process.platform === "darwin";
40
+ const theme = useTheme();
39
41
  const [scrollOffset, setScrollOffset] = useState(0);
40
42
  const isAutoFollowRef = useRef(true);
41
43
  const totalLinesRef = useRef(0);
@@ -146,7 +148,7 @@ const InteractiveShell = React.memo(({
146
148
  }
147
149
  return;
148
150
  }
149
- if (key.ctrl && input === "f") {
151
+ if (key.ctrl && input === "f" || key.escape) {
150
152
  if (isAgentControlled && isRunning) {
151
153
  return;
152
154
  }
@@ -215,7 +217,7 @@ const InteractiveShell = React.memo(({
215
217
  }
216
218
  }, { isActive: isFocused });
217
219
  const getBorderColor = () => {
218
- if (isRunning) return "#00ccff";
220
+ if (isRunning) return theme.accent;
219
221
  if (error || exitCode !== void 0 && exitCode !== 0) return "#ff3366";
220
222
  return "#00cc66";
221
223
  };
@@ -235,7 +237,7 @@ const InteractiveShell = React.memo(({
235
237
  if (currentCols < SAFE_MIN_WIDTH || currentRows < SAFE_MIN_HEIGHT) {
236
238
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Terminal too small"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, currentCols, "x", currentRows));
237
239
  }
238
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color, bold: true }, getStatusIcon(), " Shell"), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " ", truncatedCommand, " "), /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "[current working directory ", formatCwdWithContext(cwd, remoteContext), "]")), cleanOutput && /* @__PURE__ */ React.createElement(Box, { height: maxVisibleLines, overflow: "hidden" }, /* @__PURE__ */ React.createElement(Text, { wrap: "wrap" }, cleanOutput)), isFocused && isRunning && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, null, isAgentControlled ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "#9945FF", bold: true }, "[ AGENT CONTROL ] "), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, isMac ? "Cmd+I" : "Alt+I", " to disable | Shift+\u2191\u2193 scroll | PgUp/PgDn page")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "#ffaa00", bold: true }, "[ FOCUS MODE ] "), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, "Ctrl+F exit ", onWarpifySession ? `| ${isMac ? "Cmd+E / Ctrl+E" : "Alt+E"} warpify ` : "", "| ", isMac ? "Cmd+I" : "Alt+I", " agent | Shift+\u2191\u2193 scroll"))), scrollOffset > 0 && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "\u2191 Scrolled ", scrollOffset, " line", scrollOffset > 1 ? "s" : "", " from bottom (viewing history)"))), error && !isRunning && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#ff3366", wrap: "wrap" }, error.replace(/\r/g, "")))), isRunning && !isFocused && /* @__PURE__ */ React.createElement(Box, { marginLeft: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "ctrl + f to focus")));
240
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color, bold: true }, getStatusIcon(), " Shell"), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " ", truncatedCommand, " "), /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "[current working directory ", formatCwdWithContext(cwd, remoteContext), "]")), cleanOutput && /* @__PURE__ */ React.createElement(Box, { height: maxVisibleLines, overflow: "hidden" }, /* @__PURE__ */ React.createElement(Text, { wrap: "wrap" }, cleanOutput)), isFocused && isRunning && /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, null, isAgentControlled ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "#9945FF", bold: true }, "[ AGENT CONTROL ] "), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, isMac ? "Cmd+I" : "Alt+I", " to disable | Shift+\u2191\u2193 scroll | PgUp/PgDn page")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "#ffaa00", bold: true }, "[ FOCUS MODE ] "), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, "ESC / Ctrl+F exit ", onWarpifySession ? `| ${isMac ? "Cmd+E / Ctrl+E" : "Alt+E"} tunnel ` : "", "| ", isMac ? "Cmd+I" : "Alt+I", " agent | Shift+\u2191\u2193 scroll"))), scrollOffset > 0 && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "\u2191 Scrolled ", scrollOffset, " line", scrollOffset > 1 ? "s" : "", " from bottom (viewing history)"))), error && !isRunning && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#ff3366", wrap: "wrap" }, error.replace(/\r/g, "")))), isRunning && !isFocused && /* @__PURE__ */ React.createElement(Box, { marginLeft: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "ctrl + f to focus")));
239
241
  });
240
242
  export {
241
243
  InteractiveShell
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/InteractiveShell.tsx"],"sourcesContent":["import React, { useEffect, useState, useRef } from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport Spinner from 'ink-spinner';\r\nimport stripAnsi from 'strip-ansi';\r\nimport { encodeKeyToAnsi } from '../../utils/ansi-encoder.js';\r\nimport { processTerminalOutput } from '../../utils/terminal-output.js';\r\nimport { getTerminalDimensions, TERMINAL_HEIGHT_CONSTANTS } from '../../hooks/useTerminalDimensions.js';\r\nimport { sanitizeUnicode } from '../../utils/unicode-sanitizer.js';\r\n\r\ninterface InteractiveShellProps {\r\n command: string;\r\n cwd: string;\r\n isRunning: boolean;\r\n output: string;\r\n exitCode?: number;\r\n error?: string;\r\n onInput?: (input: string) => void;\r\n isFocused: boolean;\r\n onFocusChange: (focused: boolean) => void;\r\n /** Whether to show a blinking cursor in focus mode (default: true) */\r\n showCursor?: boolean;\r\n onSignal?: (signal: string) => void;\r\n /** Callback to handle terminal resize */\r\n onResize?: (cols: number, rows: number) => void;\r\n /** Remote context for SSH/WSL/Docker environments */\r\n remoteContext?: string;\r\n /** Unique shell ID for agent control mode */\r\n shellId?: string;\r\n /** Whether agent control mode is enabled */\r\n isAgentControlled?: boolean;\r\n /** Callback when agent control mode is toggled */\r\n onToggleAgentControl?: () => void;\r\n /** Callback to warpify (attach to) the current session */\r\n onWarpifySession?: () => void;\r\n}\r\n\r\n/**\r\n * Format CWD with remote context prefix for SSH/Docker/WSL environments.\r\n */\r\nfunction formatCwdWithContext(cwd: string, remoteContext?: string): string {\r\n if (remoteContext && cwd) {\r\n return `${remoteContext}:${cwd.replace(/\\\\/g, '/')}`;\r\n }\r\n return cwd;\r\n}\r\n\r\n/**\r\n * Truncate long commands for display to prevent rendering issues\r\n * @param command - The command string to truncate\r\n * @param maxLength - Maximum length before truncation (default: 80)\r\n * @returns Truncated command with ellipsis if needed\r\n */\r\nfunction truncateCommand(command: string, maxLength: number = 80): string {\r\n if (!command || command.length <= maxLength) return command;\r\n return command.substring(0, maxLength) + '...';\r\n}\r\n\r\nexport const InteractiveShell: React.FC<InteractiveShellProps> = React.memo(({\r\n command,\r\n cwd,\r\n isRunning,\r\n output,\r\n exitCode,\r\n error,\r\n onInput,\r\n isFocused,\r\n onFocusChange,\r\n onSignal,\r\n onResize,\r\n remoteContext,\r\n shellId,\r\n isAgentControlled = false,\r\n showCursor = true,\r\n onToggleAgentControl,\r\n onWarpifySession\r\n}) => {\r\n const isMac = process.platform === 'darwin';\r\n\r\n // Scroll offset for viewing previous output (0 = at bottom/following latest)\r\n const [scrollOffset, setScrollOffset] = useState(0);\r\n // Track if user has manually scrolled (to disable auto-follow)\r\n const isAutoFollowRef = useRef(true);\r\n // Track total lines for scroll calculations\r\n const totalLinesRef = useRef(0);\r\n\r\n // Blinking cursor state\r\n const [cursorVisible, setCursorVisible] = useState(true);\r\n\r\n // Handle cursor blinking\r\n useEffect(() => {\r\n if (!isFocused || !showCursor) return;\r\n\r\n const interval = setInterval(() => {\r\n setCursorVisible(v => !v);\r\n }, 530); // 530ms is a standard blink rate\r\n\r\n return () => clearInterval(interval);\r\n }, [isFocused, showCursor]);\r\n\r\n // Calculate visible lines based on focus mode and terminal height\r\n // Uses centralized terminal dimensions for consistency\r\n const terminalRows = process.stdout.rows || 24;\r\n const {\r\n INPUT_BOX_RESERVED_HEIGHT,\r\n SAFETY_BUFFER,\r\n MIN_STREAMING_LINES\r\n } = TERMINAL_HEIGHT_CONSTANTS;\r\n\r\n // Shell-specific chrome: header (2), borders (2), focus indicator (1)\r\n const shellChrome = 5;\r\n\r\n // In focus mode, InputBox is hidden so we don't reserve space for it\r\n // Reserve enough to target ~23 visible lines in a typical terminal\r\n const totalReservedFocused = 12; // Header, footer, focus indicator, and safety buffer\r\n const totalReservedUnfocused = INPUT_BOX_RESERVED_HEIGHT + shellChrome + SAFETY_BUFFER;\r\n\r\n // Calculate max visible lines based on focus state\r\n // Calculate max visible lines based on focus state\r\n // User Requirement: Max 75% of screen height\r\n const maxAllowedHeight = Math.floor(terminalRows * 0.75);\r\n\r\n const availableForShellFocused = Math.max(\r\n MIN_STREAMING_LINES,\r\n Math.min(terminalRows - totalReservedFocused, maxAllowedHeight)\r\n );\r\n\r\n const availableForShellUnfocused = Math.max(MIN_STREAMING_LINES, terminalRows - totalReservedUnfocused);\r\n\r\n // When agent controlled, use only half the available height to make room for AI panel\r\n // SAFEGUARD: Ensure we don't calculate a negative or zero height, and maintain a minimum usable area\r\n const rawAgentHeight = Math.floor(availableForShellFocused / 2);\r\n const agentControlledHeight = Math.max(MIN_STREAMING_LINES, rawAgentHeight);\r\n\r\n // If even the minimum agent height would push us beyond available space (very small terminal),\r\n // we might need to compromise. But for now, ensuring it's at least MIN_STREAMING_LINES (3) is key.\r\n const maxVisibleLines = isFocused\r\n ? (isAgentControlled\r\n ? agentControlledHeight // Already clamped above\r\n : Math.max(MIN_STREAMING_LINES, availableForShellFocused)) // Focus mode: use most of terminal\r\n : Math.max(1, Math.min(5, availableForShellUnfocused)); // Unfocused: show at most 5 lines, at least 1\r\n\r\n // Handle terminal resize events\r\n useEffect(() => {\r\n if (!onResize || !isRunning) return;\r\n\r\n const handleResize = () => {\r\n const cols = process.stdout.columns || 80;\r\n const rows = process.stdout.rows || 24;\r\n onResize(cols, rows);\r\n };\r\n\r\n handleResize();\r\n process.stdout.on('resize', handleResize);\r\n return () => {\r\n process.stdout.off('resize', handleResize);\r\n };\r\n }, [onResize, isRunning]);\r\n\r\n // Get processed output and calculate total lines\r\n // Memoize this to prevent expensive re-processing on every render/blink\r\n // Only re-process if output changes or cursor state changes\r\n const { lines, total } = React.useMemo(() => {\r\n if (!output) return { lines: [] as string[], total: 0 };\r\n\r\n const highlightCursor = isFocused && showCursor && cursorVisible;\r\n const processedOutput = processTerminalOutput(output, { highlightCursor });\r\n const splitLines = processedOutput.split('\\n');\r\n return { lines: splitLines, total: splitLines.length };\r\n }, [output, isFocused, showCursor, cursorVisible]);\r\n\r\n // Helper to access lines (replacing getProcessedOutput call)\r\n // const getProcessedOutput = () => ... REMOVED\r\n\r\n // Get visible lines based on scroll offset\r\n const getVisibleOutput = () => {\r\n // const { lines, total } = getProcessedOutput(); // Replaced by useMemo above\r\n totalLinesRef.current = total;\r\n\r\n if (total === 0) return '';\r\n\r\n // SAFETY: Use current terminal width for wrapping calculations\r\n const cols = process.stdout.columns || 80;\r\n\r\n // STRICT HEIGHT CALCULATION\r\n // We must count *visual* lines (how many rows a line takes up when wrapped)\r\n // otherwise a single long line could blow out our height budget\r\n let visualLineCount = 0;\r\n const visualLinesMap: number[] = new Array(lines.length).fill(1);\r\n\r\n // 1. Calculate visual height of all lines\r\n // precise calculation is expensive, so we estimate: length / columns\r\n for (let i = 0; i < lines.length; i++) {\r\n const line = lines[i];\r\n const stripped = stripAnsi(line || '');\r\n // Minimum 1 row, ceil(length/cols) for wrapping\r\n // We add a small buffer for safety margin in calculation logic\r\n const rows = Math.max(1, Math.ceil(stripped.length / cols));\r\n visualLinesMap[i] = rows;\r\n }\r\n\r\n // 2. Determine which lines to show based on scrollOffset\r\n // We work backwards from the bottom (latest output)\r\n\r\n const visibleLinesToRender: string[] = [];\r\n let currentHeight = 0;\r\n\r\n // Apply scroll offset - skip N visual lines from bottom\r\n // This is a simplified approach: we skip logical lines until we pass the visual offset\r\n // Ideally we would skip pixel-perfect but logical-line skipping is safer for now\r\n let linesToSkip = scrollOffset;\r\n let startIndex = lines.length - 1;\r\n\r\n // Adjust start index based on scroll\r\n while (linesToSkip > 0 && startIndex >= 0) {\r\n // We treat scrollOffset as \"logical lines\" for consistency with existing UX controls\r\n // or we could treat it as visual lines? \r\n // Existing behavior: scrollOffset is logical lines. Let's keep that for predictable scrolling.\r\n linesToSkip--;\r\n startIndex--;\r\n }\r\n\r\n // 3. Collect lines that fit in maxVisibleLines\r\n for (let i = startIndex; i >= 0; i--) {\r\n const lineRows = visualLinesMap[i];\r\n\r\n // If adding this line exceeds max height, we stop\r\n // Exception: Always show at least one line if it's the only one\r\n if (currentHeight + lineRows > maxVisibleLines && visibleLinesToRender.length > 0) {\r\n break;\r\n }\r\n\r\n visibleLinesToRender.unshift(lines[i]);\r\n currentHeight += lineRows;\r\n }\r\n\r\n return visibleLinesToRender.join('\\n');\r\n };\r\n\r\n // Reset scroll offset when exiting focus mode\r\n useEffect(() => {\r\n if (!isFocused) {\r\n setScrollOffset(0);\r\n isAutoFollowRef.current = true;\r\n }\r\n }, [isFocused]);\r\n\r\n // Track previous total lines to adjust scroll offset when new output arrives\r\n const prevTotalLinesRef = useRef(0);\r\n\r\n // When new output comes in and user is scrolled up, adjust offset to maintain position\r\n useEffect(() => {\r\n // const { total } = getProcessedOutput(); // Replaced by useMemo\r\n const prevTotal = prevTotalLinesRef.current;\r\n\r\n if (prevTotal > 0 && total > prevTotal && !isAutoFollowRef.current && scrollOffset > 0) {\r\n // New lines were added while user is scrolled up\r\n // Increase scroll offset by the number of new lines to stay at the same position\r\n const newLines = total - prevTotal;\r\n const maxOffset = Math.max(0, total - maxVisibleLines);\r\n setScrollOffset(prev => Math.min(prev + newLines, maxOffset));\r\n }\r\n\r\n prevTotalLinesRef.current = total;\r\n }, [output, maxVisibleLines, total]);\r\n\r\n // Handle input - PTY mode sends everything immediately\r\n useInput((input, key) => {\r\n if (!isFocused) return;\r\n\r\n // Alt+E: Warpify session - detect and attach to remote session\r\n // Mac: Cmd+E (key.meta) OR Ctrl+E (fallback if Cmd intercepted)\r\n // Win: Alt+E (key.meta)\r\n // Note: Ctrl+E might come in as input='\\u0005' (ASCII 5) rather than key.ctrl+e\r\n const isCtrlE = (key.ctrl && input.toLowerCase() === 'e') || input === '\\u0005';\r\n if ((key.meta && input.toLowerCase() === 'e') || isCtrlE) {\r\n if (onWarpifySession) {\r\n onWarpifySession();\r\n }\r\n return;\r\n }\r\n\r\n // Alt+I: Toggle agent control mode\r\n // Mac: Cmd+I (key.meta)\r\n if (key.meta && input.toLowerCase() === 'i') {\r\n if (onToggleAgentControl) {\r\n onToggleAgentControl();\r\n }\r\n return;\r\n }\r\n\r\n // Global: Ctrl+F to exit focus mode\r\n // BUT: Block if agent controlled AND still running\r\n if (key.ctrl && input === 'f') {\r\n if (isAgentControlled && isRunning) {\r\n // Blocked - can't exit focus while agent controlled and running\r\n // User must first disable agent control with Alt+I or wait for completion\r\n return;\r\n }\r\n onFocusChange(false);\r\n return;\r\n }\r\n\r\n // Scroll controls (only in focus mode, don't send to PTY)\r\n // Shift+Up: Scroll up one line\r\n if (key.shift && key.upArrow) {\r\n const maxOffset = Math.max(0, totalLinesRef.current - maxVisibleLines);\r\n if (scrollOffset < maxOffset) {\r\n isAutoFollowRef.current = false;\r\n setScrollOffset(prev => Math.min(prev + 1, maxOffset));\r\n }\r\n return;\r\n }\r\n\r\n // Shift+Down: Scroll down one line\r\n if (key.shift && key.downArrow) {\r\n if (scrollOffset > 0) {\r\n setScrollOffset(prev => {\r\n const newOffset = Math.max(prev - 1, 0);\r\n if (newOffset === 0) {\r\n isAutoFollowRef.current = true;\r\n }\r\n return newOffset;\r\n });\r\n }\r\n return;\r\n }\r\n\r\n // Page Up: Scroll up one page\r\n if (key.pageUp) {\r\n const maxOffset = Math.max(0, totalLinesRef.current - maxVisibleLines);\r\n if (scrollOffset < maxOffset) {\r\n isAutoFollowRef.current = false;\r\n setScrollOffset(prev => Math.min(prev + maxVisibleLines, maxOffset));\r\n }\r\n return;\r\n }\r\n\r\n // Page Down: Scroll down one page\r\n if (key.pageDown) {\r\n if (scrollOffset > 0) {\r\n setScrollOffset(prev => {\r\n const newOffset = Math.max(prev - maxVisibleLines, 0);\r\n if (newOffset === 0) {\r\n isAutoFollowRef.current = true;\r\n }\r\n return newOffset;\r\n });\r\n }\r\n return;\r\n }\r\n\r\n // Below this point, we need onInput to send to PTY\r\n if (!onInput) return;\r\n\r\n // Ctrl+C - send to PTY AND signal backend for forceful termination\r\n if (key.ctrl && input === 'c') {\r\n onInput('\\x03');\r\n if (onSignal) {\r\n onSignal('SIGINT');\r\n }\r\n return;\r\n }\r\n\r\n // Ctrl+D - EOF\r\n if (key.ctrl && input === 'd') {\r\n onInput('\\x04');\r\n return;\r\n }\r\n\r\n // Ctrl+Z - Suspend\r\n if (key.ctrl && input === 'z') {\r\n onInput('\\x1A');\r\n return;\r\n }\r\n\r\n // Encode and send all keys immediately\r\n const encoded = encodeKeyToAnsi(input, key);\r\n if (encoded) {\r\n onInput(encoded);\r\n }\r\n }, { isActive: isFocused });\r\n\r\n // Determine border color and icon based on status\r\n const getBorderColor = () => {\r\n if (isRunning) return '#00ccff';\r\n if (error || (exitCode !== undefined && exitCode !== 0)) return '#ff3366';\r\n return '#00cc66';\r\n };\r\n\r\n const getStatusIcon = () => {\r\n if (isRunning) return <Spinner type=\"dots\" />;\r\n if (error || (exitCode !== undefined && exitCode !== 0)) return '✗';\r\n return '✓';\r\n };\r\n\r\n const color = getBorderColor();\r\n const visibleOutput = getVisibleOutput();\r\n // Strip ANSI codes then sanitize problematic Unicode (emojis, wide chars)\r\n // to prevent layout issues caused by character width mismatches\r\n const cleanOutput = sanitizeUnicode(visibleOutput);\r\n const truncatedCommand = truncateCommand(command, 80);\r\n\r\n // SAFEGUARD: Check for critically small dimensions that crash Yoga WASM\r\n // Yoga can crash with \"memory access out of bounds\" if containers have negative computed size\r\n // affecting Ink's Box rendering.\r\n const currentCols = process.stdout.columns || 80;\r\n const currentRows = process.stdout.rows || 24;\r\n const SAFE_MIN_WIDTH = 15;\r\n const SAFE_MIN_HEIGHT = 5;\r\n\r\n if (currentCols < SAFE_MIN_WIDTH || currentRows < SAFE_MIN_HEIGHT) {\r\n return (\r\n <Box flexDirection=\"column\" padding={1}>\r\n <Text color=\"yellow\">Terminal too small</Text>\r\n <Text dimColor>{currentCols}x{currentRows}</Text>\r\n </Box>\r\n );\r\n }\r\n\r\n return (\r\n <Box flexDirection=\"column\" marginBottom={1}>\r\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor={color} paddingX={1}>\r\n {/* Header */}\r\n <Box marginBottom={1}>\r\n <Text color={color} bold>\r\n {getStatusIcon()} Shell\r\n </Text>\r\n <Text color=\"#666666\"> {truncatedCommand} </Text>\r\n <Text color=\"#666666\" dimColor>[current working directory {formatCwdWithContext(cwd, remoteContext)}]</Text>\r\n </Box>\r\n\r\n {/* Output area */}\r\n {/* Output area */}\r\n {cleanOutput && (\r\n <Box height={maxVisibleLines} overflow=\"hidden\">\r\n <Text wrap=\"wrap\">{cleanOutput}</Text>\r\n </Box>\r\n )}\r\n\r\n {/* Focus mode indicator */}\r\n {isFocused && isRunning && (\r\n <Box marginTop={1} flexDirection=\"column\">\r\n <Box>\r\n {isAgentControlled ? (\r\n <>\r\n <Text color=\"#9945FF\" bold>[ AGENT CONTROL ] </Text>\r\n <Text color=\"#666666\">\r\n {isMac ? 'Cmd+I' : 'Alt+I'} to disable | Shift+↑↓ scroll | PgUp/PgDn page\r\n </Text>\r\n </>\r\n ) : (\r\n <>\r\n <Text color=\"#ffaa00\" bold>[ FOCUS MODE ] </Text>\r\n <Text color=\"#666666\">\r\n Ctrl+F exit {onWarpifySession ? `| ${isMac ? 'Cmd+E / Ctrl+E' : 'Alt+E'} warpify ` : ''}| {isMac ? 'Cmd+I' : 'Alt+I'} agent | Shift+↑↓ scroll\r\n </Text>\r\n </>\r\n )}\r\n </Box>\r\n {scrollOffset > 0 && (\r\n <Box>\r\n <Text color=\"#666666\" dimColor>\r\n ↑ Scrolled {scrollOffset} line{scrollOffset > 1 ? 's' : ''} from bottom (viewing history)\r\n </Text>\r\n </Box>\r\n )}\r\n </Box>\r\n )}\r\n\r\n {/* Error display */}\r\n {error && !isRunning && (\r\n <Box marginTop={1}>\r\n <Text color=\"#ff3366\" wrap=\"wrap\">{error.replace(/\\r/g, '')}</Text>\r\n </Box>\r\n )}\r\n </Box>\r\n {isRunning && !isFocused && (\r\n <Box marginLeft={1}>\r\n <Text color=\"#666666\" dimColor>ctrl + f to focus</Text>\r\n </Box>\r\n )}\r\n </Box>\r\n );\r\n});\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/InteractiveShell.tsx"],"sourcesContent":["import React, { useEffect, useState, useRef } from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport Spinner from 'ink-spinner';\r\nimport stripAnsi from 'strip-ansi';\r\nimport { encodeKeyToAnsi } from '../../utils/ansi-encoder.js';\r\nimport { processTerminalOutput } from '../../utils/terminal-output.js';\r\nimport { getTerminalDimensions, TERMINAL_HEIGHT_CONSTANTS } from '../../hooks/useTerminalDimensions.js';\r\nimport { sanitizeUnicode } from '../../utils/unicode-sanitizer.js';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface InteractiveShellProps {\r\n command: string;\r\n cwd: string;\r\n isRunning: boolean;\r\n output: string;\r\n exitCode?: number;\r\n error?: string;\r\n onInput?: (input: string) => void;\r\n isFocused: boolean;\r\n onFocusChange: (focused: boolean) => void;\r\n /** Whether to show a blinking cursor in focus mode (default: true) */\r\n showCursor?: boolean;\r\n onSignal?: (signal: string) => void;\r\n /** Callback to handle terminal resize */\r\n onResize?: (cols: number, rows: number) => void;\r\n /** Remote context for SSH/WSL/Docker environments */\r\n remoteContext?: string;\r\n /** Unique shell ID for agent control mode */\r\n shellId?: string;\r\n /** Whether agent control mode is enabled */\r\n isAgentControlled?: boolean;\r\n /** Callback when agent control mode is toggled */\r\n onToggleAgentControl?: () => void;\r\n /** Callback to warpify (attach to) the current session */\r\n onWarpifySession?: () => void;\r\n}\r\n\r\n/**\r\n * Format CWD with remote context prefix for SSH/Docker/WSL environments.\r\n */\r\nfunction formatCwdWithContext(cwd: string, remoteContext?: string): string {\r\n if (remoteContext && cwd) {\r\n return `${remoteContext}:${cwd.replace(/\\\\/g, '/')}`;\r\n }\r\n return cwd;\r\n}\r\n\r\n/**\r\n * Truncate long commands for display to prevent rendering issues\r\n * @param command - The command string to truncate\r\n * @param maxLength - Maximum length before truncation (default: 80)\r\n * @returns Truncated command with ellipsis if needed\r\n */\r\nfunction truncateCommand(command: string, maxLength: number = 80): string {\r\n if (!command || command.length <= maxLength) return command;\r\n return command.substring(0, maxLength) + '...';\r\n}\r\n\r\nexport const InteractiveShell: React.FC<InteractiveShellProps> = React.memo(({\r\n command,\r\n cwd,\r\n isRunning,\r\n output,\r\n exitCode,\r\n error,\r\n onInput,\r\n isFocused,\r\n onFocusChange,\r\n onSignal,\r\n onResize,\r\n remoteContext,\r\n shellId,\r\n isAgentControlled = false,\r\n showCursor = true,\r\n onToggleAgentControl,\r\n onWarpifySession\r\n}) => {\r\n const isMac = process.platform === 'darwin';\r\n const theme = useTheme();\r\n\r\n // Scroll offset for viewing previous output (0 = at bottom/following latest)\r\n const [scrollOffset, setScrollOffset] = useState(0);\r\n // Track if user has manually scrolled (to disable auto-follow)\r\n const isAutoFollowRef = useRef(true);\r\n // Track total lines for scroll calculations\r\n const totalLinesRef = useRef(0);\r\n\r\n // Blinking cursor state\r\n const [cursorVisible, setCursorVisible] = useState(true);\r\n\r\n // Handle cursor blinking\r\n useEffect(() => {\r\n if (!isFocused || !showCursor) return;\r\n\r\n const interval = setInterval(() => {\r\n setCursorVisible(v => !v);\r\n }, 530); // 530ms is a standard blink rate\r\n\r\n return () => clearInterval(interval);\r\n }, [isFocused, showCursor]);\r\n\r\n // Calculate visible lines based on focus mode and terminal height\r\n // Uses centralized terminal dimensions for consistency\r\n const terminalRows = process.stdout.rows || 24;\r\n const {\r\n INPUT_BOX_RESERVED_HEIGHT,\r\n SAFETY_BUFFER,\r\n MIN_STREAMING_LINES\r\n } = TERMINAL_HEIGHT_CONSTANTS;\r\n\r\n // Shell-specific chrome: header (2), borders (2), focus indicator (1)\r\n const shellChrome = 5;\r\n\r\n // In focus mode, InputBox is hidden so we don't reserve space for it\r\n // Reserve enough to target ~23 visible lines in a typical terminal\r\n const totalReservedFocused = 12; // Header, footer, focus indicator, and safety buffer\r\n const totalReservedUnfocused = INPUT_BOX_RESERVED_HEIGHT + shellChrome + SAFETY_BUFFER;\r\n\r\n // Calculate max visible lines based on focus state\r\n // Calculate max visible lines based on focus state\r\n // User Requirement: Max 75% of screen height\r\n const maxAllowedHeight = Math.floor(terminalRows * 0.75);\r\n\r\n const availableForShellFocused = Math.max(\r\n MIN_STREAMING_LINES,\r\n Math.min(terminalRows - totalReservedFocused, maxAllowedHeight)\r\n );\r\n\r\n const availableForShellUnfocused = Math.max(MIN_STREAMING_LINES, terminalRows - totalReservedUnfocused);\r\n\r\n // When agent controlled, use only half the available height to make room for AI panel\r\n // SAFEGUARD: Ensure we don't calculate a negative or zero height, and maintain a minimum usable area\r\n const rawAgentHeight = Math.floor(availableForShellFocused / 2);\r\n const agentControlledHeight = Math.max(MIN_STREAMING_LINES, rawAgentHeight);\r\n\r\n // If even the minimum agent height would push us beyond available space (very small terminal),\r\n // we might need to compromise. But for now, ensuring it's at least MIN_STREAMING_LINES (3) is key.\r\n const maxVisibleLines = isFocused\r\n ? (isAgentControlled\r\n ? agentControlledHeight // Already clamped above\r\n : Math.max(MIN_STREAMING_LINES, availableForShellFocused)) // Focus mode: use most of terminal\r\n : Math.max(1, Math.min(5, availableForShellUnfocused)); // Unfocused: show at most 5 lines, at least 1\r\n\r\n // Handle terminal resize events\r\n useEffect(() => {\r\n if (!onResize || !isRunning) return;\r\n\r\n const handleResize = () => {\r\n const cols = process.stdout.columns || 80;\r\n const rows = process.stdout.rows || 24;\r\n onResize(cols, rows);\r\n };\r\n\r\n handleResize();\r\n process.stdout.on('resize', handleResize);\r\n return () => {\r\n process.stdout.off('resize', handleResize);\r\n };\r\n }, [onResize, isRunning]);\r\n\r\n // Get processed output and calculate total lines\r\n // Memoize this to prevent expensive re-processing on every render/blink\r\n // Only re-process if output changes or cursor state changes\r\n const { lines, total } = React.useMemo(() => {\r\n if (!output) return { lines: [] as string[], total: 0 };\r\n\r\n const highlightCursor = isFocused && showCursor && cursorVisible;\r\n const processedOutput = processTerminalOutput(output, { highlightCursor });\r\n const splitLines = processedOutput.split('\\n');\r\n return { lines: splitLines, total: splitLines.length };\r\n }, [output, isFocused, showCursor, cursorVisible]);\r\n\r\n // Helper to access lines (replacing getProcessedOutput call)\r\n // const getProcessedOutput = () => ... REMOVED\r\n\r\n // Get visible lines based on scroll offset\r\n const getVisibleOutput = () => {\r\n // const { lines, total } = getProcessedOutput(); // Replaced by useMemo above\r\n totalLinesRef.current = total;\r\n\r\n if (total === 0) return '';\r\n\r\n // SAFETY: Use current terminal width for wrapping calculations\r\n const cols = process.stdout.columns || 80;\r\n\r\n // STRICT HEIGHT CALCULATION\r\n // We must count *visual* lines (how many rows a line takes up when wrapped)\r\n // otherwise a single long line could blow out our height budget\r\n let visualLineCount = 0;\r\n const visualLinesMap: number[] = new Array(lines.length).fill(1);\r\n\r\n // 1. Calculate visual height of all lines\r\n // precise calculation is expensive, so we estimate: length / columns\r\n for (let i = 0; i < lines.length; i++) {\r\n const line = lines[i];\r\n const stripped = stripAnsi(line || '');\r\n // Minimum 1 row, ceil(length/cols) for wrapping\r\n // We add a small buffer for safety margin in calculation logic\r\n const rows = Math.max(1, Math.ceil(stripped.length / cols));\r\n visualLinesMap[i] = rows;\r\n }\r\n\r\n // 2. Determine which lines to show based on scrollOffset\r\n // We work backwards from the bottom (latest output)\r\n\r\n const visibleLinesToRender: string[] = [];\r\n let currentHeight = 0;\r\n\r\n // Apply scroll offset - skip N visual lines from bottom\r\n // This is a simplified approach: we skip logical lines until we pass the visual offset\r\n // Ideally we would skip pixel-perfect but logical-line skipping is safer for now\r\n let linesToSkip = scrollOffset;\r\n let startIndex = lines.length - 1;\r\n\r\n // Adjust start index based on scroll\r\n while (linesToSkip > 0 && startIndex >= 0) {\r\n // We treat scrollOffset as \"logical lines\" for consistency with existing UX controls\r\n // or we could treat it as visual lines? \r\n // Existing behavior: scrollOffset is logical lines. Let's keep that for predictable scrolling.\r\n linesToSkip--;\r\n startIndex--;\r\n }\r\n\r\n // 3. Collect lines that fit in maxVisibleLines\r\n for (let i = startIndex; i >= 0; i--) {\r\n const lineRows = visualLinesMap[i];\r\n\r\n // If adding this line exceeds max height, we stop\r\n // Exception: Always show at least one line if it's the only one\r\n if (currentHeight + lineRows > maxVisibleLines && visibleLinesToRender.length > 0) {\r\n break;\r\n }\r\n\r\n visibleLinesToRender.unshift(lines[i]);\r\n currentHeight += lineRows;\r\n }\r\n\r\n return visibleLinesToRender.join('\\n');\r\n };\r\n\r\n // Reset scroll offset when exiting focus mode\r\n useEffect(() => {\r\n if (!isFocused) {\r\n setScrollOffset(0);\r\n isAutoFollowRef.current = true;\r\n }\r\n }, [isFocused]);\r\n\r\n // Track previous total lines to adjust scroll offset when new output arrives\r\n const prevTotalLinesRef = useRef(0);\r\n\r\n // When new output comes in and user is scrolled up, adjust offset to maintain position\r\n useEffect(() => {\r\n // const { total } = getProcessedOutput(); // Replaced by useMemo\r\n const prevTotal = prevTotalLinesRef.current;\r\n\r\n if (prevTotal > 0 && total > prevTotal && !isAutoFollowRef.current && scrollOffset > 0) {\r\n // New lines were added while user is scrolled up\r\n // Increase scroll offset by the number of new lines to stay at the same position\r\n const newLines = total - prevTotal;\r\n const maxOffset = Math.max(0, total - maxVisibleLines);\r\n setScrollOffset(prev => Math.min(prev + newLines, maxOffset));\r\n }\r\n\r\n prevTotalLinesRef.current = total;\r\n }, [output, maxVisibleLines, total]);\r\n\r\n // Handle input - PTY mode sends everything immediately\r\n useInput((input, key) => {\r\n if (!isFocused) return;\r\n\r\n // Alt+E: Warpify session - detect and attach to remote session\r\n // Mac: Cmd+E (key.meta) OR Ctrl+E (fallback if Cmd intercepted)\r\n // Win: Alt+E (key.meta)\r\n // Note: Ctrl+E might come in as input='\\u0005' (ASCII 5) rather than key.ctrl+e\r\n const isCtrlE = (key.ctrl && input.toLowerCase() === 'e') || input === '\\u0005';\r\n if ((key.meta && input.toLowerCase() === 'e') || isCtrlE) {\r\n if (onWarpifySession) {\r\n onWarpifySession();\r\n }\r\n return;\r\n }\r\n\r\n // Alt+I: Toggle agent control mode\r\n // Mac: Cmd+I (key.meta)\r\n if (key.meta && input.toLowerCase() === 'i') {\r\n if (onToggleAgentControl) {\r\n onToggleAgentControl();\r\n }\r\n return;\r\n }\r\n\r\n // Global: Ctrl+F or ESC to exit focus mode\r\n // BUT: Block if agent controlled AND still running\r\n if ((key.ctrl && input === 'f') || key.escape) {\r\n if (isAgentControlled && isRunning) {\r\n // Blocked - can't exit focus while agent controlled and running\r\n // User must first disable agent control with Alt+I or wait for completion\r\n return;\r\n }\r\n onFocusChange(false);\r\n return;\r\n }\r\n\r\n // Scroll controls (only in focus mode, don't send to PTY)\r\n // Shift+Up: Scroll up one line\r\n if (key.shift && key.upArrow) {\r\n const maxOffset = Math.max(0, totalLinesRef.current - maxVisibleLines);\r\n if (scrollOffset < maxOffset) {\r\n isAutoFollowRef.current = false;\r\n setScrollOffset(prev => Math.min(prev + 1, maxOffset));\r\n }\r\n return;\r\n }\r\n\r\n // Shift+Down: Scroll down one line\r\n if (key.shift && key.downArrow) {\r\n if (scrollOffset > 0) {\r\n setScrollOffset(prev => {\r\n const newOffset = Math.max(prev - 1, 0);\r\n if (newOffset === 0) {\r\n isAutoFollowRef.current = true;\r\n }\r\n return newOffset;\r\n });\r\n }\r\n return;\r\n }\r\n\r\n // Page Up: Scroll up one page\r\n if (key.pageUp) {\r\n const maxOffset = Math.max(0, totalLinesRef.current - maxVisibleLines);\r\n if (scrollOffset < maxOffset) {\r\n isAutoFollowRef.current = false;\r\n setScrollOffset(prev => Math.min(prev + maxVisibleLines, maxOffset));\r\n }\r\n return;\r\n }\r\n\r\n // Page Down: Scroll down one page\r\n if (key.pageDown) {\r\n if (scrollOffset > 0) {\r\n setScrollOffset(prev => {\r\n const newOffset = Math.max(prev - maxVisibleLines, 0);\r\n if (newOffset === 0) {\r\n isAutoFollowRef.current = true;\r\n }\r\n return newOffset;\r\n });\r\n }\r\n return;\r\n }\r\n\r\n // Below this point, we need onInput to send to PTY\r\n if (!onInput) return;\r\n\r\n // Ctrl+C - send to PTY AND signal backend for forceful termination\r\n if (key.ctrl && input === 'c') {\r\n onInput('\\x03');\r\n if (onSignal) {\r\n onSignal('SIGINT');\r\n }\r\n return;\r\n }\r\n\r\n // Ctrl+D - EOF\r\n if (key.ctrl && input === 'd') {\r\n onInput('\\x04');\r\n return;\r\n }\r\n\r\n // Ctrl+Z - Suspend\r\n if (key.ctrl && input === 'z') {\r\n onInput('\\x1A');\r\n return;\r\n }\r\n\r\n // Encode and send all keys immediately\r\n const encoded = encodeKeyToAnsi(input, key);\r\n if (encoded) {\r\n onInput(encoded);\r\n }\r\n }, { isActive: isFocused });\r\n\r\n // Determine border color and icon based on status\r\n const getBorderColor = () => {\r\n if (isRunning) return theme.accent;\r\n if (error || (exitCode !== undefined && exitCode !== 0)) return '#ff3366';\r\n return '#00cc66';\r\n };\r\n\r\n const getStatusIcon = () => {\r\n if (isRunning) return <Spinner type=\"dots\" />;\r\n if (error || (exitCode !== undefined && exitCode !== 0)) return '✗';\r\n return '✓';\r\n };\r\n\r\n const color = getBorderColor();\r\n const visibleOutput = getVisibleOutput();\r\n // Strip ANSI codes then sanitize problematic Unicode (emojis, wide chars)\r\n // to prevent layout issues caused by character width mismatches\r\n const cleanOutput = sanitizeUnicode(visibleOutput);\r\n const truncatedCommand = truncateCommand(command, 80);\r\n\r\n // SAFEGUARD: Check for critically small dimensions that crash Yoga WASM\r\n // Yoga can crash with \"memory access out of bounds\" if containers have negative computed size\r\n // affecting Ink's Box rendering.\r\n const currentCols = process.stdout.columns || 80;\r\n const currentRows = process.stdout.rows || 24;\r\n const SAFE_MIN_WIDTH = 15;\r\n const SAFE_MIN_HEIGHT = 5;\r\n\r\n if (currentCols < SAFE_MIN_WIDTH || currentRows < SAFE_MIN_HEIGHT) {\r\n return (\r\n <Box flexDirection=\"column\" padding={1}>\r\n <Text color=\"yellow\">Terminal too small</Text>\r\n <Text dimColor>{currentCols}x{currentRows}</Text>\r\n </Box>\r\n );\r\n }\r\n\r\n return (\r\n <Box flexDirection=\"column\" marginBottom={1}>\r\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor={color} paddingX={1}>\r\n {/* Header */}\r\n <Box marginBottom={1}>\r\n <Text color={color} bold>\r\n {getStatusIcon()} Shell\r\n </Text>\r\n <Text color=\"#666666\"> {truncatedCommand} </Text>\r\n <Text color=\"#666666\" dimColor>[current working directory {formatCwdWithContext(cwd, remoteContext)}]</Text>\r\n </Box>\r\n\r\n {/* Output area */}\r\n {/* Output area */}\r\n {cleanOutput && (\r\n <Box height={maxVisibleLines} overflow=\"hidden\">\r\n <Text wrap=\"wrap\">{cleanOutput}</Text>\r\n </Box>\r\n )}\r\n\r\n {/* Focus mode indicator */}\r\n {isFocused && isRunning && (\r\n <Box marginTop={1} flexDirection=\"column\">\r\n <Box>\r\n {isAgentControlled ? (\r\n <>\r\n <Text color=\"#9945FF\" bold>[ AGENT CONTROL ] </Text>\r\n <Text color=\"#666666\">\r\n {isMac ? 'Cmd+I' : 'Alt+I'} to disable | Shift+↑↓ scroll | PgUp/PgDn page\r\n </Text>\r\n </>\r\n ) : (\r\n <>\r\n <Text color=\"#ffaa00\" bold>[ FOCUS MODE ] </Text>\r\n <Text color=\"#666666\">\r\n ESC / Ctrl+F exit {onWarpifySession ? `| ${isMac ? 'Cmd+E / Ctrl+E' : 'Alt+E'} tunnel ` : ''}| {isMac ? 'Cmd+I' : 'Alt+I'} agent | Shift+↑↓ scroll\r\n </Text>\r\n </>\r\n )}\r\n </Box>\r\n {scrollOffset > 0 && (\r\n <Box>\r\n <Text color=\"#666666\" dimColor>\r\n ↑ Scrolled {scrollOffset} line{scrollOffset > 1 ? 's' : ''} from bottom (viewing history)\r\n </Text>\r\n </Box>\r\n )}\r\n </Box>\r\n )}\r\n\r\n {/* Error display */}\r\n {error && !isRunning && (\r\n <Box marginTop={1}>\r\n <Text color=\"#ff3366\" wrap=\"wrap\">{error.replace(/\\r/g, '')}</Text>\r\n </Box>\r\n )}\r\n </Box>\r\n {isRunning && !isFocused && (\r\n <Box marginLeft={1}>\r\n <Text color=\"#666666\" dimColor>ctrl + f to focus</Text>\r\n </Box>\r\n )}\r\n </Box>\r\n );\r\n});\r\n"],"mappings":"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","names":[]}
@@ -1,6 +1,8 @@
1
1
  import React from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
+ import { useTheme } from "../theme.js";
3
4
  const KeyboardHelp = ({ onClose }) => {
5
+ const theme = useTheme();
4
6
  useInput((input, key) => {
5
7
  if (key.escape || input === "?") {
6
8
  onClose();
@@ -15,6 +17,7 @@ const KeyboardHelp = ({ onClose }) => {
15
17
  { key: "Ctrl+Enter", action: "Insert newline" },
16
18
  { key: "\\ + Enter", action: "Insert newline fallback" },
17
19
  { key: "Ctrl+D", action: "Toggle command mode" },
20
+ { key: "Ctrl+P", action: "Toggle plan mode" },
18
21
  { key: "Tab", action: "Autocomplete" },
19
22
  { key: "Ctrl+T", action: "Toggle auto-accept" },
20
23
  { key: "Ctrl+C", action: "Cancel / Exit" },
@@ -28,7 +31,7 @@ const KeyboardHelp = ({ onClose }) => {
28
31
  { key: "/", action: "Slash command" },
29
32
  { key: "?", action: "Show help" }
30
33
  ];
31
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "double", borderColor: "#00ccff", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "Keyboard Shortcuts")), shortcuts.map((s, idx) => /* @__PURE__ */ React.createElement(Box, { key: idx, marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00cc66", bold: true }, s.key.padEnd(15)), /* @__PURE__ */ React.createElement(Text, { color: "#ffffff" }, s.action))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "Press ? or ESC to close")));
34
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "double", borderColor: theme.accent, paddingX: 1, marginY: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: theme.accent, bold: true }, "Keyboard Shortcuts")), shortcuts.map((s, idx) => /* @__PURE__ */ React.createElement(Box, { key: idx, marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00cc66", bold: true }, s.key.padEnd(15)), /* @__PURE__ */ React.createElement(Text, { color: "#ffffff" }, s.action))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "Press ? or ESC to close")));
32
35
  };
33
36
  export {
34
37
  KeyboardHelp
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/KeyboardHelp.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\n\r\ninterface KeyboardHelpProps {\r\n onClose: () => void;\r\n}\r\n\r\nexport const KeyboardHelp: React.FC<KeyboardHelpProps> = ({ onClose }) => {\r\n useInput((input, key) => {\r\n if (key.escape || input === '?') {\r\n onClose();\r\n }\r\n });\r\n\r\n const isMac = process.platform === 'darwin';\r\n const metaKey = isMac ? 'Cmd' : 'Alt';\r\n const altKey = isMac ? 'Option' : 'Alt';\r\n\r\n // Note: Some shortcuts like Ctrl+C are standard across terminals including Mac (Ctrl+C, not Cmd+C)\r\n // But navigation/editing often uses Cmd on Mac.\r\n\r\n const shortcuts = [\r\n { key: 'Enter', action: 'Submit message' },\r\n { key: `${altKey}+Enter`, action: 'Insert newline (Shift+Enter remap)' },\r\n { key: 'Ctrl+Enter', action: 'Insert newline' },\r\n { key: '\\\\ + Enter', action: 'Insert newline fallback' },\r\n { key: 'Ctrl+D', action: 'Toggle command mode' },\r\n { key: 'Tab', action: 'Autocomplete' },\r\n { key: 'Ctrl+T', action: 'Toggle auto-accept' },\r\n { key: 'Ctrl+C', action: 'Cancel / Exit' },\r\n { key: `${metaKey}+Z`, action: 'Undo' }, // undo\r\n { key: isMac ? 'Cmd/Ctrl+E' : 'Alt+E', action: 'Warpify (Interactive)' },\r\n { key: `${isMac ? 'Cmd' : 'Ctrl'}+Left`, action: 'Home / Word Back' }, // Simplified\r\n { key: `${metaKey}+V`, action: 'Paste File / Image' },\r\n { key: `${metaKey}+X`, action: 'Remove Last File' },\r\n { key: '/', action: 'Slash command' },\r\n { key: '?', action: 'Show help' }\r\n ];\r\n\r\n return (\r\n <Box flexDirection=\"column\" borderStyle=\"double\" borderColor=\"#00ccff\" paddingX={1} marginY={1}>\r\n <Box marginBottom={1}>\r\n <Text color=\"#00ccff\" bold>Keyboard Shortcuts</Text>\r\n </Box>\r\n\r\n {shortcuts.map((s, idx) => (\r\n <Box key={idx} marginBottom={1}>\r\n <Text color=\"#00cc66\" bold>{s.key.padEnd(15)}</Text>\r\n <Text color=\"#ffffff\">{s.action}</Text>\r\n </Box>\r\n ))}\r\n\r\n <Box marginTop={1}>\r\n <Text color=\"#666666\" dimColor>\r\n Press ? or ESC to close\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"AAAA,OAAO,WAAW;AAClB,SAAS,KAAK,MAAM,gBAAgB;AAM7B,MAAM,eAA4C,CAAC,EAAE,QAAQ,MAAM;AACxE,WAAS,CAAC,OAAO,QAAQ;AACvB,QAAI,IAAI,UAAU,UAAU,KAAK;AAC/B,cAAQ;AAAA,IACV;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,QAAQ,aAAa;AACnC,QAAM,UAAU,QAAQ,QAAQ;AAChC,QAAM,SAAS,QAAQ,WAAW;AAKlC,QAAM,YAAY;AAAA,IAChB,EAAE,KAAK,SAAS,QAAQ,iBAAiB;AAAA,IACzC,EAAE,KAAK,GAAG,MAAM,UAAU,QAAQ,qCAAqC;AAAA,IACvE,EAAE,KAAK,cAAc,QAAQ,iBAAiB;AAAA,IAC9C,EAAE,KAAK,cAAc,QAAQ,0BAA0B;AAAA,IACvD,EAAE,KAAK,UAAU,QAAQ,sBAAsB;AAAA,IAC/C,EAAE,KAAK,OAAO,QAAQ,eAAe;AAAA,IACrC,EAAE,KAAK,UAAU,QAAQ,qBAAqB;AAAA,IAC9C,EAAE,KAAK,UAAU,QAAQ,gBAAgB;AAAA,IACzC,EAAE,KAAK,GAAG,OAAO,MAAM,QAAQ,OAAO;AAAA;AAAA,IACtC,EAAE,KAAK,QAAQ,eAAe,SAAS,QAAQ,wBAAwB;AAAA,IACvE,EAAE,KAAK,GAAG,QAAQ,QAAQ,MAAM,SAAS,QAAQ,mBAAmB;AAAA;AAAA,IACpE,EAAE,KAAK,GAAG,OAAO,MAAM,QAAQ,qBAAqB;AAAA,IACpD,EAAE,KAAK,GAAG,OAAO,MAAM,QAAQ,mBAAmB;AAAA,IAClD,EAAE,KAAK,KAAK,QAAQ,gBAAgB;AAAA,IACpC,EAAE,KAAK,KAAK,QAAQ,YAAY;AAAA,EAClC;AAEA,SACE,oCAAC,OAAI,eAAc,UAAS,aAAY,UAAS,aAAY,WAAU,UAAU,GAAG,SAAS,KAC3F,oCAAC,OAAI,cAAc,KACjB,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,oBAAkB,CAC/C,GAEC,UAAU,IAAI,CAAC,GAAG,QACjB,oCAAC,OAAI,KAAK,KAAK,cAAc,KAC3B,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAE,EAAE,IAAI,OAAO,EAAE,CAAE,GAC7C,oCAAC,QAAK,OAAM,aAAW,EAAE,MAAO,CAClC,CACD,GAED,oCAAC,OAAI,WAAW,KACd,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,yBAE/B,CACF,CACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/KeyboardHelp.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface KeyboardHelpProps {\r\n onClose: () => void;\r\n}\r\n\r\nexport const KeyboardHelp: React.FC<KeyboardHelpProps> = ({ onClose }) => {\r\n const theme = useTheme();\r\n useInput((input, key) => {\r\n if (key.escape || input === '?') {\r\n onClose();\r\n }\r\n });\r\n\r\n const isMac = process.platform === 'darwin';\r\n const metaKey = isMac ? 'Cmd' : 'Alt';\r\n const altKey = isMac ? 'Option' : 'Alt';\r\n\r\n // Note: Some shortcuts like Ctrl+C are standard across terminals including Mac (Ctrl+C, not Cmd+C)\r\n // But navigation/editing often uses Cmd on Mac.\r\n\r\n const shortcuts = [\r\n { key: 'Enter', action: 'Submit message' },\r\n { key: `${altKey}+Enter`, action: 'Insert newline (Shift+Enter remap)' },\r\n { key: 'Ctrl+Enter', action: 'Insert newline' },\r\n { key: '\\\\ + Enter', action: 'Insert newline fallback' },\r\n { key: 'Ctrl+D', action: 'Toggle command mode' },\r\n { key: 'Ctrl+P', action: 'Toggle plan mode' },\r\n { key: 'Tab', action: 'Autocomplete' },\r\n { key: 'Ctrl+T', action: 'Toggle auto-accept' },\r\n { key: 'Ctrl+C', action: 'Cancel / Exit' },\r\n { key: `${metaKey}+Z`, action: 'Undo' }, // undo\r\n { key: isMac ? 'Cmd/Ctrl+E' : 'Alt+E', action: 'Warpify (Interactive)' },\r\n { key: `${isMac ? 'Cmd' : 'Ctrl'}+Left`, action: 'Home / Word Back' }, // Simplified\r\n { key: `${metaKey}+V`, action: 'Paste File / Image' },\r\n { key: `${metaKey}+X`, action: 'Remove Last File' },\r\n { key: '/', action: 'Slash command' },\r\n { key: '?', action: 'Show help' }\r\n ];\r\n\r\n return (\r\n <Box flexDirection=\"column\" borderStyle=\"double\" borderColor={theme.accent} paddingX={1} marginY={1}>\r\n <Box marginBottom={1}>\r\n <Text color={theme.accent} bold>Keyboard Shortcuts</Text>\r\n </Box>\r\n\r\n {shortcuts.map((s, idx) => (\r\n <Box key={idx} marginBottom={1}>\r\n <Text color=\"#00cc66\" bold>{s.key.padEnd(15)}</Text>\r\n <Text color=\"#ffffff\">{s.action}</Text>\r\n </Box>\r\n ))}\r\n\r\n <Box marginTop={1}>\r\n <Text color=\"#666666\" dimColor>\r\n Press ? or ESC to close\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"AAAA,OAAO,WAAW;AAClB,SAAS,KAAK,MAAM,gBAAgB;AACpC,SAAS,gBAAgB;AAMlB,MAAM,eAA4C,CAAC,EAAE,QAAQ,MAAM;AACxE,QAAM,QAAQ,SAAS;AACvB,WAAS,CAAC,OAAO,QAAQ;AACvB,QAAI,IAAI,UAAU,UAAU,KAAK;AAC/B,cAAQ;AAAA,IACV;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,QAAQ,aAAa;AACnC,QAAM,UAAU,QAAQ,QAAQ;AAChC,QAAM,SAAS,QAAQ,WAAW;AAKlC,QAAM,YAAY;AAAA,IAChB,EAAE,KAAK,SAAS,QAAQ,iBAAiB;AAAA,IACzC,EAAE,KAAK,GAAG,MAAM,UAAU,QAAQ,qCAAqC;AAAA,IACvE,EAAE,KAAK,cAAc,QAAQ,iBAAiB;AAAA,IAC9C,EAAE,KAAK,cAAc,QAAQ,0BAA0B;AAAA,IACvD,EAAE,KAAK,UAAU,QAAQ,sBAAsB;AAAA,IAC/C,EAAE,KAAK,UAAU,QAAQ,mBAAmB;AAAA,IAC5C,EAAE,KAAK,OAAO,QAAQ,eAAe;AAAA,IACrC,EAAE,KAAK,UAAU,QAAQ,qBAAqB;AAAA,IAC9C,EAAE,KAAK,UAAU,QAAQ,gBAAgB;AAAA,IACzC,EAAE,KAAK,GAAG,OAAO,MAAM,QAAQ,OAAO;AAAA;AAAA,IACtC,EAAE,KAAK,QAAQ,eAAe,SAAS,QAAQ,wBAAwB;AAAA,IACvE,EAAE,KAAK,GAAG,QAAQ,QAAQ,MAAM,SAAS,QAAQ,mBAAmB;AAAA;AAAA,IACpE,EAAE,KAAK,GAAG,OAAO,MAAM,QAAQ,qBAAqB;AAAA,IACpD,EAAE,KAAK,GAAG,OAAO,MAAM,QAAQ,mBAAmB;AAAA,IAClD,EAAE,KAAK,KAAK,QAAQ,gBAAgB;AAAA,IACpC,EAAE,KAAK,KAAK,QAAQ,YAAY;AAAA,EAClC;AAEA,SACE,oCAAC,OAAI,eAAc,UAAS,aAAY,UAAS,aAAa,MAAM,QAAQ,UAAU,GAAG,SAAS,KAChG,oCAAC,OAAI,cAAc,KACjB,oCAAC,QAAK,OAAO,MAAM,QAAQ,MAAI,QAAC,oBAAkB,CACpD,GAEC,UAAU,IAAI,CAAC,GAAG,QACjB,oCAAC,OAAI,KAAK,KAAK,cAAc,KAC3B,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAE,EAAE,IAAI,OAAO,EAAE,CAAE,GAC7C,oCAAC,QAAK,OAAM,aAAW,EAAE,MAAO,CAClC,CACD,GAED,oCAAC,OAAI,WAAW,KACd,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,yBAE/B,CACF,CACF;AAEJ;","names":[]}
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import Spinner from "ink-spinner";
4
4
  import { ShimmerText } from "./ShimmerText.js";
5
+ import { useTheme } from "../theme.js";
5
6
  const LOADING_MESSAGES = [
6
7
  "Thrusting",
7
8
  "Propelling",
@@ -16,9 +17,10 @@ const LOADING_MESSAGES = [
16
17
  ];
17
18
  const SPINNER_TYPES = ["dots", "dots2", "line", "star", "arc", "circle", "arrow", "bouncingBar", "bouncingBall", "earth", "moon"];
18
19
  const LoadingIndicator = () => {
20
+ const theme = useTheme();
19
21
  const messageIndex = React.useMemo(() => Math.floor(Math.random() * LOADING_MESSAGES.length), []);
20
22
  const spinnerType = React.useMemo(() => SPINNER_TYPES[Math.floor(Math.random() * SPINNER_TYPES.length)], []);
21
- return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff" }, /* @__PURE__ */ React.createElement(Spinner, { type: spinnerType })), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(ShimmerText, { text: `${LOADING_MESSAGES[messageIndex]}...`, baseColor: "#00ccff", bold: true }));
23
+ return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: theme.accent }, /* @__PURE__ */ React.createElement(Spinner, { type: spinnerType })), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(ShimmerText, { text: `${LOADING_MESSAGES[messageIndex]}...`, baseColor: theme.accent, bold: true }));
22
24
  };
23
25
  export {
24
26
  LoadingIndicator
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/LoadingIndicator.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport Spinner from 'ink-spinner';\r\nimport { ShimmerText } from './ShimmerText.js';\r\n\r\nconst LOADING_MESSAGES = [\r\n 'Thrusting',\r\n 'Propelling',\r\n 'Boosting',\r\n 'Launching',\r\n 'Intercepting',\r\n 'Slingshotting',\r\n 'Accelerating',\r\n 'Cruising',\r\n 'Warping',\r\n 'Docking'\r\n];\r\n\r\nconst SPINNER_TYPES = ['dots', 'dots2', 'line', 'star', 'arc', 'circle', 'arrow', 'bouncingBar', 'bouncingBall', 'earth', 'moon'] as const;\r\n\r\nexport const LoadingIndicator: React.FC = () => {\r\n // Use useMemo to pick random message and spinner only once, not on every render\r\n const messageIndex = React.useMemo(() => Math.floor(Math.random() * LOADING_MESSAGES.length), []);\r\n const spinnerType = React.useMemo(() => SPINNER_TYPES[Math.floor(Math.random() * SPINNER_TYPES.length)], []);\r\n\r\n return (\r\n <Box>\r\n <Text color=\"#00ccff\">\r\n <Spinner type={spinnerType} />\r\n </Text>\r\n <Text> </Text>\r\n <ShimmerText text={`${LOADING_MESSAGES[messageIndex]}...`} baseColor=\"#00ccff\" bold />\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"AAAA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,OAAO,aAAa;AACpB,SAAS,mBAAmB;AAE5B,MAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,gBAAgB,CAAC,QAAQ,SAAS,QAAQ,QAAQ,OAAO,UAAU,SAAS,eAAe,gBAAgB,SAAS,MAAM;AAEzH,MAAM,mBAA6B,MAAM;AAE9C,QAAM,eAAe,MAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,iBAAiB,MAAM,GAAG,CAAC,CAAC;AAChG,QAAM,cAAc,MAAM,QAAQ,MAAM,cAAc,KAAK,MAAM,KAAK,OAAO,IAAI,cAAc,MAAM,CAAC,GAAG,CAAC,CAAC;AAE3G,SACE,oCAAC,WACC,oCAAC,QAAK,OAAM,aACV,oCAAC,WAAQ,MAAM,aAAa,CAC9B,GACA,oCAAC,YAAK,GAAC,GACP,oCAAC,eAAY,MAAM,GAAG,iBAAiB,YAAY,CAAC,OAAO,WAAU,WAAU,MAAI,MAAC,CACtF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/LoadingIndicator.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport Spinner from 'ink-spinner';\r\nimport { ShimmerText } from './ShimmerText.js';\r\nimport { useTheme } from '../theme.js';\r\n\r\nconst LOADING_MESSAGES = [\r\n 'Thrusting',\r\n 'Propelling',\r\n 'Boosting',\r\n 'Launching',\r\n 'Intercepting',\r\n 'Slingshotting',\r\n 'Accelerating',\r\n 'Cruising',\r\n 'Warping',\r\n 'Docking'\r\n];\r\n\r\nconst SPINNER_TYPES = ['dots', 'dots2', 'line', 'star', 'arc', 'circle', 'arrow', 'bouncingBar', 'bouncingBall', 'earth', 'moon'] as const;\r\n\r\nexport const LoadingIndicator: React.FC = () => {\r\n const theme = useTheme();\r\n // Use useMemo to pick random message and spinner only once, not on every render\r\n const messageIndex = React.useMemo(() => Math.floor(Math.random() * LOADING_MESSAGES.length), []);\r\n const spinnerType = React.useMemo(() => SPINNER_TYPES[Math.floor(Math.random() * SPINNER_TYPES.length)], []);\r\n\r\n return (\r\n <Box>\r\n <Text color={theme.accent}>\r\n <Spinner type={spinnerType} />\r\n </Text>\r\n <Text> </Text>\r\n <ShimmerText text={`${LOADING_MESSAGES[messageIndex]}...`} baseColor={theme.accent} bold />\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"AAAA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,OAAO,aAAa;AACpB,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AAEzB,MAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,gBAAgB,CAAC,QAAQ,SAAS,QAAQ,QAAQ,OAAO,UAAU,SAAS,eAAe,gBAAgB,SAAS,MAAM;AAEzH,MAAM,mBAA6B,MAAM;AAC9C,QAAM,QAAQ,SAAS;AAEvB,QAAM,eAAe,MAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,iBAAiB,MAAM,GAAG,CAAC,CAAC;AAChG,QAAM,cAAc,MAAM,QAAQ,MAAM,cAAc,KAAK,MAAM,KAAK,OAAO,IAAI,cAAc,MAAM,CAAC,GAAG,CAAC,CAAC;AAE3G,SACE,oCAAC,WACC,oCAAC,QAAK,OAAO,MAAM,UACjB,oCAAC,WAAQ,MAAM,aAAa,CAC9B,GACA,oCAAC,YAAK,GAAC,GACP,oCAAC,eAAY,MAAM,GAAG,iBAAiB,YAAY,CAAC,OAAO,WAAW,MAAM,QAAQ,MAAI,MAAC,CAC3F;AAEJ;","names":[]}
@@ -1,41 +1,91 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import { MultiLineInput } from "./MultiLineInput.js";
1
+ import React, { useRef, useEffect, useCallback } from "react";
2
+ import { Box, Text, useStdin } from "ink";
3
+ import { TextEditor } from "./TextEditor.js";
4
+ import { useTheme } from "../theme.js";
5
+ import * as readline from "readline";
4
6
  const MCPAddScreen = ({ onAdd, onCancel, validateConfig }) => {
7
+ const theme = useTheme();
8
+ const { stdin, setRawMode } = useStdin();
5
9
  const [input, setInput] = React.useState("");
6
10
  const [validationResult, setValidationResult] = React.useState(null);
7
- const handleSubmit = () => {
8
- if (!input.trim()) {
11
+ const emitInitRef = useRef(false);
12
+ const inputRef = useRef(input);
13
+ const validationRef = useRef(validationResult);
14
+ const escTimerRef = useRef(null);
15
+ useEffect(() => {
16
+ inputRef.current = input;
17
+ }, [input]);
18
+ useEffect(() => {
19
+ validationRef.current = validationResult;
20
+ }, [validationResult]);
21
+ const handleSubmit = useCallback(() => {
22
+ const currentInput = inputRef.current;
23
+ if (!currentInput.trim()) {
9
24
  setValidationResult({ valid: false, error: "Please paste your MCP server configuration" });
10
25
  return;
11
26
  }
12
- const result = validateConfig(input.trim());
27
+ const result = validateConfig(currentInput.trim());
13
28
  setValidationResult(result);
14
29
  if (result.valid && result.config) {
15
30
  onAdd(result.config);
16
31
  }
17
- };
32
+ }, [validateConfig, onAdd]);
18
33
  const handleInputChange = (value) => {
19
34
  setInput(value);
20
35
  if (validationResult) {
21
36
  setValidationResult(null);
22
37
  }
23
38
  };
39
+ useEffect(() => {
40
+ if (!stdin) return;
41
+ setRawMode(true);
42
+ if (!emitInitRef.current) {
43
+ readline.emitKeypressEvents(stdin);
44
+ emitInitRef.current = true;
45
+ }
46
+ const handler = (_str, key) => {
47
+ if (!key) return;
48
+ const kname = key.name || "";
49
+ const ctrl = !!key.ctrl;
50
+ if (kname !== "escape" && escTimerRef.current) {
51
+ clearTimeout(escTimerRef.current);
52
+ escTimerRef.current = null;
53
+ }
54
+ if (kname === "escape") {
55
+ if (escTimerRef.current) clearTimeout(escTimerRef.current);
56
+ escTimerRef.current = setTimeout(() => {
57
+ escTimerRef.current = null;
58
+ onCancel();
59
+ }, 50);
60
+ return;
61
+ }
62
+ if (ctrl && kname === "s") {
63
+ handleSubmit();
64
+ return;
65
+ }
66
+ };
67
+ stdin.on("keypress", handler);
68
+ return () => {
69
+ stdin.off("keypress", handler);
70
+ if (escTimerRef.current) clearTimeout(escTimerRef.current);
71
+ };
72
+ }, [stdin, setRawMode, onCancel, handleSubmit]);
24
73
  const exampleConfig = `"chrome-devtools": {
25
74
  "command": "npx",
26
75
  "args": ["-y", "chrome-devtools-mcp@latest"]
27
76
  }`;
28
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff", bold: true }, "Add MCP Server"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Example format:")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#888888" }, exampleConfig)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Paste your MCP server config:")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: validationResult?.valid ? "#00cc66" : validationResult?.error ? "#ff6666" : "#555555", paddingX: 1 }, /* @__PURE__ */ React.createElement(
29
- MultiLineInput,
77
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: theme.accent, bold: true }, "Add MCP Server"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Example format:")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "#888888" }, exampleConfig)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "gray" }, "Paste your MCP server config:")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: validationResult?.valid ? "#00cc66" : validationResult?.error ? "#ff6666" : "#555555", paddingX: 1 }, /* @__PURE__ */ React.createElement(
78
+ TextEditor,
30
79
  {
31
80
  value: input,
32
81
  onChange: handleInputChange,
33
- onSubmit: handleSubmit,
34
- placeholder: '{"name": "...", "command": "...", "args": [...]}',
35
82
  minHeight: 3,
36
- maxHeight: 5
83
+ maxHeight: 5,
84
+ placeholder: '{"name": "...", "command": "...", "args": [...]}',
85
+ isActive: true,
86
+ width: Math.max(20, (process.stdout.columns || 80) - 10)
37
87
  }
38
- )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, !validationResult && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press Enter to add the server"), validationResult?.valid && /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, 'Adding server "', validationResult.config?.name, '"...'), validationResult?.error && /* @__PURE__ */ React.createElement(Text, { color: "#ff6666" }, "Error: ", validationResult.error)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "[Enter] Add Server [ESC] Cancel")));
88
+ )), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, !validationResult && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press ", /* @__PURE__ */ React.createElement(Text, { color: "#ffd700" }, "Ctrl+S"), " to add the server"), validationResult?.valid && /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, 'Adding server "', validationResult.config?.name, '"...'), validationResult?.error && /* @__PURE__ */ React.createElement(Text, { color: "#ff6666" }, "Error: ", validationResult.error)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "[Ctrl+S] Add Server [ESC] Cancel")));
39
89
  };
40
90
  export {
41
91
  MCPAddScreen
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/MCPAddScreen.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport { MultiLineInput } from './MultiLineInput.js';\r\n\r\ninterface MCPAddScreenProps {\r\n onAdd: (config: any) => void;\r\n onCancel: () => void;\r\n validateConfig: (jsonString: string) => { valid: boolean; config?: any; error?: string };\r\n}\r\n\r\nexport const MCPAddScreen: React.FC<MCPAddScreenProps> = ({ onAdd, onCancel, validateConfig }) => {\r\n const [input, setInput] = React.useState('');\r\n const [validationResult, setValidationResult] = React.useState<{ valid: boolean; config?: any; error?: string } | null>(null);\r\n // Note: ESC key handling is done in App.tsx\r\n\r\n const handleSubmit = () => {\r\n if (!input.trim()) {\r\n setValidationResult({ valid: false, error: 'Please paste your MCP server configuration' });\r\n return;\r\n }\r\n\r\n // Validate and add immediately on first Enter\r\n const result = validateConfig(input.trim());\r\n setValidationResult(result);\r\n\r\n if (result.valid && result.config) {\r\n // Add the server immediately\r\n onAdd(result.config);\r\n }\r\n };\r\n\r\n const handleInputChange = (value: string) => {\r\n setInput(value);\r\n // Reset validation when input changes\r\n if (validationResult) {\r\n setValidationResult(null);\r\n }\r\n };\r\n\r\n const exampleConfig = `\"chrome-devtools\": {\r\n \"command\": \"npx\",\r\n \"args\": [\"-y\", \"chrome-devtools-mcp@latest\"]\r\n}`;\r\n\r\n return (\r\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor=\"#00ccff\" paddingX={1}>\r\n <Text color=\"#00ccff\" bold>Add MCP Server</Text>\r\n\r\n <Box marginTop={1}>\r\n <Text color=\"gray\">Example format:</Text>\r\n </Box>\r\n\r\n <Box marginTop={1} borderStyle=\"round\" borderColor=\"gray\" paddingX={1}>\r\n <Text color=\"#888888\">{exampleConfig}</Text>\r\n </Box>\r\n\r\n <Box marginTop={1}>\r\n <Text color=\"gray\">Paste your MCP server config:</Text>\r\n </Box>\r\n\r\n <Box marginTop={1} borderStyle=\"round\" borderColor={validationResult?.valid ? '#00cc66' : (validationResult?.error ? '#ff6666' : '#555555')} paddingX={1}>\r\n <MultiLineInput\r\n value={input}\r\n onChange={handleInputChange}\r\n onSubmit={handleSubmit}\r\n placeholder='{\"name\": \"...\", \"command\": \"...\", \"args\": [...]}'\r\n minHeight={3}\r\n maxHeight={5}\r\n />\r\n </Box>\r\n\r\n {/* Validation result */}\r\n <Box marginTop={1}>\r\n {!validationResult && (\r\n <Text dimColor>Press Enter to add the server</Text>\r\n )}\r\n {validationResult?.valid && (\r\n <Text color=\"#00cc66\">Adding server \"{validationResult.config?.name}\"...</Text>\r\n )}\r\n {validationResult?.error && (\r\n <Text color=\"#ff6666\">Error: {validationResult.error}</Text>\r\n )}\r\n </Box>\r\n\r\n <Box marginTop={1}>\r\n <Text dimColor>[Enter] Add Server [ESC] Cancel</Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"AAAA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,SAAS,sBAAsB;AAQxB,MAAM,eAA4C,CAAC,EAAE,OAAO,UAAU,eAAe,MAAM;AAC9F,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAkE,IAAI;AAG5H,QAAM,eAAe,MAAM;AACvB,QAAI,CAAC,MAAM,KAAK,GAAG;AACf,0BAAoB,EAAE,OAAO,OAAO,OAAO,6CAA6C,CAAC;AACzF;AAAA,IACJ;AAGA,UAAM,SAAS,eAAe,MAAM,KAAK,CAAC;AAC1C,wBAAoB,MAAM;AAE1B,QAAI,OAAO,SAAS,OAAO,QAAQ;AAE/B,YAAM,OAAO,MAAM;AAAA,IACvB;AAAA,EACJ;AAEA,QAAM,oBAAoB,CAAC,UAAkB;AACzC,aAAS,KAAK;AAEd,QAAI,kBAAkB;AAClB,0BAAoB,IAAI;AAAA,IAC5B;AAAA,EACJ;AAEA,QAAM,gBAAgB;AAAA;AAAA;AAAA;AAKtB,SACI,oCAAC,OAAI,eAAc,UAAS,aAAY,SAAQ,aAAY,WAAU,UAAU,KAC5E,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,gBAAc,GAEzC,oCAAC,OAAI,WAAW,KACZ,oCAAC,QAAK,OAAM,UAAO,iBAAe,CACtC,GAEA,oCAAC,OAAI,WAAW,GAAG,aAAY,SAAQ,aAAY,QAAO,UAAU,KAChE,oCAAC,QAAK,OAAM,aAAW,aAAc,CACzC,GAEA,oCAAC,OAAI,WAAW,KACZ,oCAAC,QAAK,OAAM,UAAO,+BAA6B,CACpD,GAEA,oCAAC,OAAI,WAAW,GAAG,aAAY,SAAQ,aAAa,kBAAkB,QAAQ,YAAa,kBAAkB,QAAQ,YAAY,WAAY,UAAU,KACnJ;AAAA,IAAC;AAAA;AAAA,MACG,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU;AAAA,MACV,aAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA;AAAA,EACf,CACJ,GAGA,oCAAC,OAAI,WAAW,KACX,CAAC,oBACE,oCAAC,QAAK,UAAQ,QAAC,+BAA6B,GAE/C,kBAAkB,SACf,oCAAC,QAAK,OAAM,aAAU,mBAAgB,iBAAiB,QAAQ,MAAK,MAAI,GAE3E,kBAAkB,SACf,oCAAC,QAAK,OAAM,aAAU,WAAQ,iBAAiB,KAAM,CAE7D,GAEA,oCAAC,OAAI,WAAW,KACZ,oCAAC,QAAK,UAAQ,QAAC,kCAAgC,CACnD,CACJ;AAER;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/MCPAddScreen.tsx"],"sourcesContent":["import React, { useRef, useEffect, useCallback } from 'react';\r\nimport { Box, Text, useStdin } from 'ink';\r\nimport { TextEditor } from './TextEditor.js';\r\nimport { useTheme } from '../theme.js';\r\nimport * as readline from 'readline';\r\n\r\ninterface MCPAddScreenProps {\r\n onAdd: (config: any) => void;\r\n onCancel: () => void;\r\n validateConfig: (jsonString: string) => { valid: boolean; config?: any; error?: string };\r\n}\r\n\r\nexport const MCPAddScreen: React.FC<MCPAddScreenProps> = ({ onAdd, onCancel, validateConfig }) => {\r\n const theme = useTheme();\r\n const { stdin, setRawMode } = useStdin();\r\n const [input, setInput] = React.useState('');\r\n const [validationResult, setValidationResult] = React.useState<{ valid: boolean; config?: any; error?: string } | null>(null);\r\n const emitInitRef = useRef(false);\r\n const inputRef = useRef(input);\r\n const validationRef = useRef(validationResult);\r\n const escTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n useEffect(() => { inputRef.current = input; }, [input]);\r\n useEffect(() => { validationRef.current = validationResult; }, [validationResult]);\r\n\r\n const handleSubmit = useCallback(() => {\r\n const currentInput = inputRef.current;\r\n if (!currentInput.trim()) {\r\n setValidationResult({ valid: false, error: 'Please paste your MCP server configuration' });\r\n return;\r\n }\r\n\r\n const result = validateConfig(currentInput.trim());\r\n setValidationResult(result);\r\n\r\n if (result.valid && result.config) {\r\n onAdd(result.config);\r\n }\r\n }, [validateConfig, onAdd]);\r\n\r\n const handleInputChange = (value: string) => {\r\n setInput(value);\r\n if (validationResult) {\r\n setValidationResult(null);\r\n }\r\n };\r\n\r\n // Raw stdin handler for Ctrl+S (submit) and ESC (cancel)\r\n useEffect(() => {\r\n if (!stdin) return;\r\n setRawMode(true);\r\n\r\n if (!emitInitRef.current) {\r\n readline.emitKeypressEvents(stdin);\r\n emitInitRef.current = true;\r\n }\r\n\r\n const handler = (_str: string | undefined, key: any) => {\r\n if (!key) return;\r\n const kname = key.name || '';\r\n const ctrl = !!key.ctrl;\r\n\r\n // Any non-escape keypress cancels a pending ESC (it was a meta combo like Option+Backspace)\r\n if (kname !== 'escape' && escTimerRef.current) {\r\n clearTimeout(escTimerRef.current);\r\n escTimerRef.current = null;\r\n }\r\n\r\n // ESC — debounce to distinguish standalone ESC from meta sequences\r\n if (kname === 'escape') {\r\n if (escTimerRef.current) clearTimeout(escTimerRef.current);\r\n escTimerRef.current = setTimeout(() => {\r\n escTimerRef.current = null;\r\n onCancel();\r\n }, 50);\r\n return;\r\n }\r\n\r\n // Ctrl+S — submit\r\n if (ctrl && kname === 's') {\r\n handleSubmit();\r\n return;\r\n }\r\n };\r\n\r\n stdin.on('keypress', handler);\r\n return () => {\r\n stdin.off('keypress', handler);\r\n if (escTimerRef.current) clearTimeout(escTimerRef.current);\r\n };\r\n }, [stdin, setRawMode, onCancel, handleSubmit]);\r\n\r\n const exampleConfig = `\"chrome-devtools\": {\r\n \"command\": \"npx\",\r\n \"args\": [\"-y\", \"chrome-devtools-mcp@latest\"]\r\n}`;\r\n\r\n return (\r\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor={theme.accent} paddingX={1}>\r\n <Text color={theme.accent} bold>Add MCP Server</Text>\r\n\r\n <Box marginTop={1}>\r\n <Text color=\"gray\">Example format:</Text>\r\n </Box>\r\n\r\n <Box marginTop={1} borderStyle=\"round\" borderColor=\"gray\" paddingX={1}>\r\n <Text color=\"#888888\">{exampleConfig}</Text>\r\n </Box>\r\n\r\n <Box marginTop={1}>\r\n <Text color=\"gray\">Paste your MCP server config:</Text>\r\n </Box>\r\n\r\n <Box marginTop={1} borderStyle=\"round\" borderColor={validationResult?.valid ? '#00cc66' : (validationResult?.error ? '#ff6666' : '#555555')} paddingX={1}>\r\n <TextEditor\r\n value={input}\r\n onChange={handleInputChange}\r\n minHeight={3}\r\n maxHeight={5}\r\n placeholder='{\"name\": \"...\", \"command\": \"...\", \"args\": [...]}'\r\n isActive={true}\r\n width={Math.max(20, (process.stdout.columns || 80) - 10)}\r\n />\r\n </Box>\r\n\r\n {/* Validation result */}\r\n <Box marginTop={1}>\r\n {!validationResult && (\r\n <Text dimColor>Press <Text color=\"#ffd700\">Ctrl+S</Text> to add the server</Text>\r\n )}\r\n {validationResult?.valid && (\r\n <Text color=\"#00cc66\">Adding server \"{validationResult.config?.name}\"...</Text>\r\n )}\r\n {validationResult?.error && (\r\n <Text color=\"#ff6666\">Error: {validationResult.error}</Text>\r\n )}\r\n </Box>\r\n\r\n <Box marginTop={1}>\r\n <Text dimColor>[Ctrl+S] Add Server [ESC] Cancel</Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"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","names":[]}
@@ -1,7 +1,9 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { MarkdownParser, wrapText } from "../../utils/markdown-parser.js";
4
+ import { useTheme } from "../theme.js";
4
5
  const MarkdownRenderer = ({ content, maxWidth = 100 }) => {
6
+ const theme = useTheme();
5
7
  const parser = useMemo(() => new MarkdownParser(), []);
6
8
  const elements = useMemo(() => parser.parse(content), [content, parser]);
7
9
  const renderInlineElements = (children) => {
@@ -15,7 +17,7 @@ const MarkdownRenderer = ({ content, maxWidth = 100 }) => {
15
17
  case "code-inline":
16
18
  return /* @__PURE__ */ React.createElement(Text, { key: idx, backgroundColor: "#2d2d2d", color: "#ff79c6" }, ` ${child.content} `);
17
19
  case "link":
18
- return /* @__PURE__ */ React.createElement(Text, { key: idx, color: "#00ccff", underline: true }, child.content);
20
+ return /* @__PURE__ */ React.createElement(Text, { key: idx, color: theme.accent, underline: true }, child.content);
19
21
  case "text":
20
22
  default:
21
23
  return /* @__PURE__ */ React.createElement(Text, { key: idx, color: "#ffffff" }, child.content);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/MarkdownRenderer.tsx"],"sourcesContent":["import React, { useMemo } from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport { MarkdownParser, ParsedElement, wrapText } from '../../utils/markdown-parser.js';\r\n\r\ninterface MarkdownRendererProps {\r\n content: string;\r\n maxWidth?: number;\r\n}\r\n\r\nexport const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, maxWidth = 100 }) => {\r\n const parser = useMemo(() => new MarkdownParser(), []);\r\n const elements = useMemo(() => parser.parse(content), [content, parser]);\r\n\r\n const renderInlineElements = (children?: ParsedElement[]): React.ReactNode => {\r\n if (!children || children.length === 0) return null;\r\n\r\n return children.map((child, idx) => {\r\n switch (child.type) {\r\n case 'bold':\r\n return (\r\n <Text key={idx} bold color=\"#00ffff\">\r\n {child.content}\r\n </Text>\r\n );\r\n case 'italic':\r\n return (\r\n <Text key={idx} italic color=\"#cccccc\">\r\n {child.content}\r\n </Text>\r\n );\r\n case 'code-inline':\r\n return (\r\n <Text key={idx} backgroundColor=\"#2d2d2d\" color=\"#ff79c6\">\r\n {` ${child.content} `}\r\n </Text>\r\n );\r\n case 'link':\r\n return (\r\n <Text key={idx} color=\"#00ccff\" underline>\r\n {child.content}\r\n </Text>\r\n );\r\n case 'text':\r\n default:\r\n return (\r\n <Text key={idx} color=\"#ffffff\">\r\n {child.content}\r\n </Text>\r\n );\r\n }\r\n });\r\n };\r\n\r\n const renderElement = (element: ParsedElement, index: number): React.ReactNode => {\r\n switch (element.type) {\r\n case 'heading': {\r\n const level = element.level || 1;\r\n const colors = ['#00ffff', '#00dddd', '#00bbbb', '#009999', '#007777', '#005555'];\r\n const color = colors[level - 1] || '#ffffff';\r\n\r\n // Wrap heading text if it's too long\r\n const availableWidth = maxWidth - 2; // -2 for padding\r\n const wrappedLines = wrapText(element.content, availableWidth, 0);\r\n\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginTop={0} marginBottom={0}>\r\n {wrappedLines.map((line, lineIdx) => (\r\n <Text key={lineIdx} bold color={color}>\r\n {line}\r\n </Text>\r\n ))}\r\n </Box>\r\n );\r\n }\r\n\r\n case 'code-block': {\r\n const lines = element.content.split('\\n');\r\n\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginBottom={0} marginTop={0}>\r\n <Box borderStyle=\"round\" borderColor=\"#666666\" paddingX={1} paddingY={0}>\r\n <Box flexDirection=\"column\">\r\n {element.language && element.language !== 'text' && (\r\n <Text color=\"#888888\" dimColor>\r\n {element.language}\r\n </Text>\r\n )}\r\n {lines.map((line, lineIdx) => (\r\n <Text key={lineIdx} color=\"#f8f8f2\">\r\n {line || ' '}\r\n </Text>\r\n ))}\r\n </Box>\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'list-item': {\r\n const indent = (element.level || 0) * 2;\r\n const bullet = element.ordered ? `${element.index}.` : '•';\r\n const bulletColor = '#00cc66';\r\n const bulletWidth = element.ordered ? 3 : 2; // \"1.\" or \"•\" + space\r\n const availableWidth = maxWidth - indent - bulletWidth - 4; // Account for all padding\r\n\r\n // Render list item with proper width constraints\r\n return (\r\n <Box key={index} flexDirection=\"row\" marginBottom={0}>\r\n <Text color=\"#666666\">{' '.repeat(indent)}</Text>\r\n <Text color={bulletColor} bold>\r\n {bullet}{' '}\r\n </Text>\r\n <Box flexShrink={1} width={availableWidth}>\r\n <Text wrap=\"wrap\">\r\n {element.children && element.children.length > 0\r\n ? renderInlineElements(element.children)\r\n : <Text color=\"#ffffff\">{element.content}</Text>}\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'blockquote': {\r\n return (\r\n <Box key={index} flexDirection=\"row\" marginBottom={0}>\r\n <Text color=\"#666666\" bold>\r\n │{' '}\r\n </Text>\r\n <Text color=\"#cccccc\" italic>\r\n {element.children && element.children.length > 0\r\n ? renderInlineElements(element.children)\r\n : element.content}\r\n </Text>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'horizontal-rule': {\r\n return (\r\n <Box key={index} marginY={0}>\r\n <Text color=\"#444444\">{'─'.repeat(Math.min(maxWidth, 80))}</Text>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'line-break': {\r\n // Use a Text element with newline instead of Box with height={0}\r\n // to prevent breaking the parent's border\r\n return <Text key={index}>{' '}</Text>;\r\n }\r\n\r\n case 'text': {\r\n // If we have inline children (formatted text), render them with proper wrapping\r\n if (element.children && element.children.length > 0) {\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginBottom={0}>\r\n <Box paddingLeft={2} width={maxWidth - 2} flexWrap=\"wrap\">\r\n <Text wrap=\"wrap\">{renderInlineElements(element.children)}</Text>\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n // Otherwise, wrap plain text if needed\r\n // Account for paddingLeft={2} in the wrapping calculation\r\n const wrappedLines = wrapText(element.content, maxWidth - 2, 0);\r\n\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginBottom={0}>\r\n {wrappedLines.map((line, lineIdx) => (\r\n <Box key={lineIdx} paddingLeft={2}>\r\n <Text color=\"#ffffff\">{line}</Text>\r\n </Box>\r\n ))}\r\n </Box>\r\n );\r\n }\r\n\r\n default:\r\n return null;\r\n }\r\n };\r\n\r\n return (\r\n <Box flexDirection=\"column\">\r\n {elements.map((element, index) => renderElement(element, index))}\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/MarkdownRenderer.tsx"],"sourcesContent":["import React, { useMemo } from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport { MarkdownParser, ParsedElement, wrapText } from '../../utils/markdown-parser.js';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface MarkdownRendererProps {\r\n content: string;\r\n maxWidth?: number;\r\n}\r\n\r\nexport const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, maxWidth = 100 }) => {\r\n const theme = useTheme();\r\n const parser = useMemo(() => new MarkdownParser(), []);\r\n const elements = useMemo(() => parser.parse(content), [content, parser]);\r\n\r\n const renderInlineElements = (children?: ParsedElement[]): React.ReactNode => {\r\n if (!children || children.length === 0) return null;\r\n\r\n return children.map((child, idx) => {\r\n switch (child.type) {\r\n case 'bold':\r\n return (\r\n <Text key={idx} bold color=\"#00ffff\">\r\n {child.content}\r\n </Text>\r\n );\r\n case 'italic':\r\n return (\r\n <Text key={idx} italic color=\"#cccccc\">\r\n {child.content}\r\n </Text>\r\n );\r\n case 'code-inline':\r\n return (\r\n <Text key={idx} backgroundColor=\"#2d2d2d\" color=\"#ff79c6\">\r\n {` ${child.content} `}\r\n </Text>\r\n );\r\n case 'link':\r\n return (\r\n <Text key={idx} color={theme.accent} underline>\r\n {child.content}\r\n </Text>\r\n );\r\n case 'text':\r\n default:\r\n return (\r\n <Text key={idx} color=\"#ffffff\">\r\n {child.content}\r\n </Text>\r\n );\r\n }\r\n });\r\n };\r\n\r\n const renderElement = (element: ParsedElement, index: number): React.ReactNode => {\r\n switch (element.type) {\r\n case 'heading': {\r\n const level = element.level || 1;\r\n const colors = ['#00ffff', '#00dddd', '#00bbbb', '#009999', '#007777', '#005555'];\r\n const color = colors[level - 1] || '#ffffff';\r\n\r\n // Wrap heading text if it's too long\r\n const availableWidth = maxWidth - 2; // -2 for padding\r\n const wrappedLines = wrapText(element.content, availableWidth, 0);\r\n\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginTop={0} marginBottom={0}>\r\n {wrappedLines.map((line, lineIdx) => (\r\n <Text key={lineIdx} bold color={color}>\r\n {line}\r\n </Text>\r\n ))}\r\n </Box>\r\n );\r\n }\r\n\r\n case 'code-block': {\r\n const lines = element.content.split('\\n');\r\n\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginBottom={0} marginTop={0}>\r\n <Box borderStyle=\"round\" borderColor=\"#666666\" paddingX={1} paddingY={0}>\r\n <Box flexDirection=\"column\">\r\n {element.language && element.language !== 'text' && (\r\n <Text color=\"#888888\" dimColor>\r\n {element.language}\r\n </Text>\r\n )}\r\n {lines.map((line, lineIdx) => (\r\n <Text key={lineIdx} color=\"#f8f8f2\">\r\n {line || ' '}\r\n </Text>\r\n ))}\r\n </Box>\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'list-item': {\r\n const indent = (element.level || 0) * 2;\r\n const bullet = element.ordered ? `${element.index}.` : '•';\r\n const bulletColor = '#00cc66';\r\n const bulletWidth = element.ordered ? 3 : 2; // \"1.\" or \"•\" + space\r\n const availableWidth = maxWidth - indent - bulletWidth - 4; // Account for all padding\r\n\r\n // Render list item with proper width constraints\r\n return (\r\n <Box key={index} flexDirection=\"row\" marginBottom={0}>\r\n <Text color=\"#666666\">{' '.repeat(indent)}</Text>\r\n <Text color={bulletColor} bold>\r\n {bullet}{' '}\r\n </Text>\r\n <Box flexShrink={1} width={availableWidth}>\r\n <Text wrap=\"wrap\">\r\n {element.children && element.children.length > 0\r\n ? renderInlineElements(element.children)\r\n : <Text color=\"#ffffff\">{element.content}</Text>}\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'blockquote': {\r\n return (\r\n <Box key={index} flexDirection=\"row\" marginBottom={0}>\r\n <Text color=\"#666666\" bold>\r\n │{' '}\r\n </Text>\r\n <Text color=\"#cccccc\" italic>\r\n {element.children && element.children.length > 0\r\n ? renderInlineElements(element.children)\r\n : element.content}\r\n </Text>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'horizontal-rule': {\r\n return (\r\n <Box key={index} marginY={0}>\r\n <Text color=\"#444444\">{'─'.repeat(Math.min(maxWidth, 80))}</Text>\r\n </Box>\r\n );\r\n }\r\n\r\n case 'line-break': {\r\n // Use a Text element with newline instead of Box with height={0}\r\n // to prevent breaking the parent's border\r\n return <Text key={index}>{' '}</Text>;\r\n }\r\n\r\n case 'text': {\r\n // If we have inline children (formatted text), render them with proper wrapping\r\n if (element.children && element.children.length > 0) {\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginBottom={0}>\r\n <Box paddingLeft={2} width={maxWidth - 2} flexWrap=\"wrap\">\r\n <Text wrap=\"wrap\">{renderInlineElements(element.children)}</Text>\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n // Otherwise, wrap plain text if needed\r\n // Account for paddingLeft={2} in the wrapping calculation\r\n const wrappedLines = wrapText(element.content, maxWidth - 2, 0);\r\n\r\n return (\r\n <Box key={index} flexDirection=\"column\" marginBottom={0}>\r\n {wrappedLines.map((line, lineIdx) => (\r\n <Box key={lineIdx} paddingLeft={2}>\r\n <Text color=\"#ffffff\">{line}</Text>\r\n </Box>\r\n ))}\r\n </Box>\r\n );\r\n }\r\n\r\n default:\r\n return null;\r\n }\r\n };\r\n\r\n return (\r\n <Box flexDirection=\"column\">\r\n {elements.map((element, index) => renderElement(element, index))}\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"AAAA,OAAO,SAAS,eAAe;AAC/B,SAAS,KAAK,YAAY;AAC1B,SAAS,gBAA+B,gBAAgB;AACxD,SAAS,gBAAgB;AAOlB,MAAM,mBAAoD,CAAC,EAAE,SAAS,WAAW,IAAI,MAAM;AAChG,QAAM,QAAQ,SAAS;AACvB,QAAM,SAAS,QAAQ,MAAM,IAAI,eAAe,GAAG,CAAC,CAAC;AACrD,QAAM,WAAW,QAAQ,MAAM,OAAO,MAAM,OAAO,GAAG,CAAC,SAAS,MAAM,CAAC;AAEvE,QAAM,uBAAuB,CAAC,aAAgD;AAC5E,QAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO;AAE/C,WAAO,SAAS,IAAI,CAAC,OAAO,QAAQ;AAClC,cAAQ,MAAM,MAAM;AAAA,QAClB,KAAK;AACH,iBACE,oCAAC,QAAK,KAAK,KAAK,MAAI,MAAC,OAAM,aACxB,MAAM,OACT;AAAA,QAEJ,KAAK;AACH,iBACE,oCAAC,QAAK,KAAK,KAAK,QAAM,MAAC,OAAM,aAC1B,MAAM,OACT;AAAA,QAEJ,KAAK;AACH,iBACE,oCAAC,QAAK,KAAK,KAAK,iBAAgB,WAAU,OAAM,aAC7C,IAAI,MAAM,OAAO,GACpB;AAAA,QAEJ,KAAK;AACH,iBACE,oCAAC,QAAK,KAAK,KAAK,OAAO,MAAM,QAAQ,WAAS,QAC3C,MAAM,OACT;AAAA,QAEJ,KAAK;AAAA,QACL;AACE,iBACE,oCAAC,QAAK,KAAK,KAAK,OAAM,aACnB,MAAM,OACT;AAAA,MAEN;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,gBAAgB,CAAC,SAAwB,UAAmC;AAChF,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK,WAAW;AACd,cAAM,QAAQ,QAAQ,SAAS;AAC/B,cAAM,SAAS,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAChF,cAAM,QAAQ,OAAO,QAAQ,CAAC,KAAK;AAGnC,cAAM,iBAAiB,WAAW;AAClC,cAAM,eAAe,SAAS,QAAQ,SAAS,gBAAgB,CAAC;AAEhE,eACE,oCAAC,OAAI,KAAK,OAAO,eAAc,UAAS,WAAW,GAAG,cAAc,KACjE,aAAa,IAAI,CAAC,MAAM,YACvB,oCAAC,QAAK,KAAK,SAAS,MAAI,MAAC,SACtB,IACH,CACD,CACH;AAAA,MAEJ;AAAA,MAEA,KAAK,cAAc;AACjB,cAAM,QAAQ,QAAQ,QAAQ,MAAM,IAAI;AAExC,eACE,oCAAC,OAAI,KAAK,OAAO,eAAc,UAAS,cAAc,GAAG,WAAW,KAClE,oCAAC,OAAI,aAAY,SAAQ,aAAY,WAAU,UAAU,GAAG,UAAU,KACpE,oCAAC,OAAI,eAAc,YAChB,QAAQ,YAAY,QAAQ,aAAa,UACxC,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAC3B,QAAQ,QACX,GAED,MAAM,IAAI,CAAC,MAAM,YAChB,oCAAC,QAAK,KAAK,SAAS,OAAM,aACvB,QAAQ,GACX,CACD,CACH,CACF,CACF;AAAA,MAEJ;AAAA,MAEA,KAAK,aAAa;AAChB,cAAM,UAAU,QAAQ,SAAS,KAAK;AACtC,cAAM,SAAS,QAAQ,UAAU,GAAG,QAAQ,KAAK,MAAM;AACvD,cAAM,cAAc;AACpB,cAAM,cAAc,QAAQ,UAAU,IAAI;AAC1C,cAAM,iBAAiB,WAAW,SAAS,cAAc;AAGzD,eACE,oCAAC,OAAI,KAAK,OAAO,eAAc,OAAM,cAAc,KACjD,oCAAC,QAAK,OAAM,aAAW,IAAI,OAAO,MAAM,CAAE,GAC1C,oCAAC,QAAK,OAAO,aAAa,MAAI,QAC3B,QAAQ,GACX,GACA,oCAAC,OAAI,YAAY,GAAG,OAAO,kBACzB,oCAAC,QAAK,MAAK,UACR,QAAQ,YAAY,QAAQ,SAAS,SAAS,IAC3C,qBAAqB,QAAQ,QAAQ,IACrC,oCAAC,QAAK,OAAM,aAAW,QAAQ,OAAQ,CAC7C,CACF,CACF;AAAA,MAEJ;AAAA,MAEA,KAAK,cAAc;AACjB,eACE,oCAAC,OAAI,KAAK,OAAO,eAAc,OAAM,cAAc,KACjD,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,UACvB,GACJ,GACA,oCAAC,QAAK,OAAM,WAAU,QAAM,QACzB,QAAQ,YAAY,QAAQ,SAAS,SAAS,IAC3C,qBAAqB,QAAQ,QAAQ,IACrC,QAAQ,OACd,CACF;AAAA,MAEJ;AAAA,MAEA,KAAK,mBAAmB;AACtB,eACE,oCAAC,OAAI,KAAK,OAAO,SAAS,KACxB,oCAAC,QAAK,OAAM,aAAW,SAAI,OAAO,KAAK,IAAI,UAAU,EAAE,CAAC,CAAE,CAC5D;AAAA,MAEJ;AAAA,MAEA,KAAK,cAAc;AAGjB,eAAO,oCAAC,QAAK,KAAK,SAAQ,GAAI;AAAA,MAChC;AAAA,MAEA,KAAK,QAAQ;AAEX,YAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAAG;AACnD,iBACE,oCAAC,OAAI,KAAK,OAAO,eAAc,UAAS,cAAc,KACpD,oCAAC,OAAI,aAAa,GAAG,OAAO,WAAW,GAAG,UAAS,UACjD,oCAAC,QAAK,MAAK,UAAQ,qBAAqB,QAAQ,QAAQ,CAAE,CAC5D,CACF;AAAA,QAEJ;AAIA,cAAM,eAAe,SAAS,QAAQ,SAAS,WAAW,GAAG,CAAC;AAE9D,eACE,oCAAC,OAAI,KAAK,OAAO,eAAc,UAAS,cAAc,KACnD,aAAa,IAAI,CAAC,MAAM,YACvB,oCAAC,OAAI,KAAK,SAAS,aAAa,KAC9B,oCAAC,QAAK,OAAM,aAAW,IAAK,CAC9B,CACD,CACH;AAAA,MAEJ;AAAA,MAEA;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAEA,SACE,oCAAC,OAAI,eAAc,YAChB,SAAS,IAAI,CAAC,SAAS,UAAU,cAAc,SAAS,KAAK,CAAC,CACjE;AAEJ;","names":[]}
@@ -3,6 +3,7 @@ import { Box, Text, useInput } from "ink";
3
3
  import { ToolExecutionMessage } from "./ToolExecutionMessage.js";
4
4
  import { MarkdownRenderer } from "./MarkdownRenderer.js";
5
5
  import { ConnectionStatusMessage } from "./ConnectionStatusMessage.js";
6
+ import { useTheme } from "../theme.js";
6
7
  function parseFileMarkers(content) {
7
8
  const fileMarkerRegex = /\[(IMAGE|FILE):\s*([^\]]+?)\s*\((gs:\/\/[^\)]+|upload failed[^\)]*|uploading[^\)]*)\)\]/g;
8
9
  const files = [];
@@ -38,7 +39,7 @@ const FileAttachmentBadge = ({ fileNumber, type, format }) => {
38
39
  /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, "[", format, "]")
39
40
  );
40
41
  };
41
- function renderTextWithFileTags(text) {
42
+ function renderTextWithFileTags(text, accentColor) {
42
43
  const tagRegex = /(@[^\s@]+)/g;
43
44
  const parts = [];
44
45
  let lastIndex = 0;
@@ -51,7 +52,7 @@ function renderTextWithFileTags(text) {
51
52
  );
52
53
  }
53
54
  parts.push(
54
- /* @__PURE__ */ React.createElement(Text, { key: key++, color: "#00ccff", bold: true }, match[1])
55
+ /* @__PURE__ */ React.createElement(Text, { key: key++, color: accentColor, bold: true }, match[1])
55
56
  );
56
57
  lastIndex = match.index + match[1].length;
57
58
  }
@@ -66,18 +67,19 @@ const MessageDisplay = React.memo(({
66
67
  message,
67
68
  fileCountBefore = 0
68
69
  }) => {
70
+ const theme = useTheme();
69
71
  const [selectedOption, setSelectedOption] = React.useState(0);
70
72
  const [isApproved, setIsApproved] = React.useState(null);
71
73
  const getRoleColor = (role) => {
72
74
  switch (role) {
73
75
  case "user":
74
- return "#00ccff";
76
+ return theme.accent;
75
77
  case "assistant":
76
78
  return "#00cc66";
77
79
  case "system":
78
80
  return "#ffaa00";
79
81
  case "tool":
80
- return "#00ccff";
82
+ return theme.accent;
81
83
  default:
82
84
  return "#ffffff";
83
85
  }
@@ -123,9 +125,9 @@ const MessageDisplay = React.memo(({
123
125
  }
124
126
  const getBorderColor = () => {
125
127
  if (message.isCommandMode) {
126
- return message.role === "assistant" ? "#00cc66" : "#00ccff";
128
+ return message.role === "assistant" ? "#00cc66" : theme.accent;
127
129
  }
128
- return message.role === "assistant" ? "#00ccff" : "#666666";
130
+ return message.role === "assistant" ? theme.accent : "#666666";
129
131
  };
130
132
  const borderColor = getBorderColor();
131
133
  const { textContent, files } = message.role === "user" && !message.isCommandMode ? parseFileMarkers(message.content) : { textContent: message.content, files: [] };
@@ -152,7 +154,7 @@ const MessageDisplay = React.memo(({
152
154
  width: "100%"
153
155
  },
154
156
  /* @__PURE__ */ React.createElement(MarkdownRenderer, { content: message.content, maxWidth: (process.stdout.columns || 120) - 6 })
155
- ) : null : /* @__PURE__ */ React.createElement(React.Fragment, null, textContent.trim() ? /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, null, renderTextWithFileTags(textContent))) : null, fileBadges.length > 0 && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2, flexDirection: "row", flexWrap: "wrap" }, fileBadges)));
157
+ ) : null : /* @__PURE__ */ React.createElement(React.Fragment, null, textContent.trim() ? /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, null, renderTextWithFileTags(textContent, theme.accent))) : null, fileBadges.length > 0 && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2, flexDirection: "row", flexWrap: "wrap" }, fileBadges)));
156
158
  });
157
159
  export {
158
160
  MessageDisplay,