aiexecode 1.0.66 → 1.0.69

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.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

Files changed (87) hide show
  1. package/config_template/settings.json +1 -3
  2. package/index.js +46 -71
  3. package/package.json +1 -12
  4. package/payload_viewer/out/404/index.html +1 -1
  5. package/payload_viewer/out/404.html +1 -1
  6. package/payload_viewer/out/index.html +1 -1
  7. package/payload_viewer/out/index.txt +1 -1
  8. package/payload_viewer/web_server.js +0 -163
  9. package/prompts/completion_judge.txt +11 -7
  10. package/src/ai_based/completion_judge.js +97 -6
  11. package/src/ai_based/orchestrator.js +71 -3
  12. package/src/ai_based/pip_package_installer.js +14 -12
  13. package/src/ai_based/pip_package_lookup.js +13 -10
  14. package/src/commands/apikey.js +8 -34
  15. package/src/commands/help.js +3 -4
  16. package/src/commands/model.js +17 -74
  17. package/src/commands/reasoning_effort.js +1 -1
  18. package/src/config/feature_flags.js +0 -12
  19. package/src/{ui → frontend}/App.js +23 -25
  20. package/src/frontend/README.md +81 -0
  21. package/src/{ui/components/SuggestionsDisplay.js → frontend/components/AutocompleteMenu.js} +3 -3
  22. package/src/{ui/components/HistoryItemDisplay.js → frontend/components/ConversationItem.js} +37 -89
  23. package/src/{ui → frontend}/components/CurrentModelView.js +3 -5
  24. package/src/{ui → frontend}/components/Footer.js +4 -6
  25. package/src/{ui → frontend}/components/Header.js +2 -5
  26. package/src/{ui/components/InputPrompt.js → frontend/components/Input.js} +16 -54
  27. package/src/frontend/components/ModelListView.js +106 -0
  28. package/src/{ui → frontend}/components/ModelUpdatedView.js +3 -5
  29. package/src/{ui → frontend}/components/SessionSpinner.js +3 -3
  30. package/src/{ui → frontend}/components/SetupWizard.js +8 -101
  31. package/src/{ui → frontend}/components/ToolApprovalPrompt.js +16 -14
  32. package/src/frontend/design/themeColors.js +42 -0
  33. package/src/{ui → frontend}/index.js +7 -7
  34. package/src/frontend/utils/inputBuffer.js +441 -0
  35. package/src/{ui/utils/markdownRenderer.js → frontend/utils/markdownParser.js} +3 -3
  36. package/src/{ui/utils/ConsolePatcher.js → frontend/utils/outputRedirector.js} +9 -9
  37. package/src/{ui/utils/codeColorizer.js → frontend/utils/syntaxHighlighter.js} +2 -3
  38. package/src/system/ai_request.js +145 -595
  39. package/src/system/code_executer.js +111 -16
  40. package/src/system/file_integrity.js +5 -7
  41. package/src/system/log.js +3 -3
  42. package/src/system/mcp_integration.js +15 -13
  43. package/src/system/output_helper.js +0 -20
  44. package/src/system/session.js +97 -23
  45. package/src/system/session_memory.js +2 -82
  46. package/src/system/system_info.js +1 -1
  47. package/src/system/ui_events.js +0 -43
  48. package/src/tools/code_editor.js +17 -2
  49. package/src/tools/file_reader.js +17 -2
  50. package/src/tools/glob.js +9 -1
  51. package/src/tools/response_message.js +0 -2
  52. package/src/tools/ripgrep.js +9 -1
  53. package/src/tools/web_downloader.js +9 -1
  54. package/src/util/config.js +3 -8
  55. package/src/util/debug_log.js +4 -11
  56. package/src/util/mcp_config_manager.js +3 -5
  57. package/src/util/output_formatter.js +0 -47
  58. package/src/util/prompt_loader.js +3 -4
  59. package/src/util/safe_fs.js +60 -0
  60. package/src/util/setup_wizard.js +1 -3
  61. package/src/util/text_formatter.js +0 -86
  62. package/src/config/claude_models.js +0 -195
  63. package/src/ui/README.md +0 -208
  64. package/src/ui/api.js +0 -167
  65. package/src/ui/components/AgenticProgressDisplay.js +0 -126
  66. package/src/ui/components/Composer.js +0 -55
  67. package/src/ui/components/LoadingIndicator.js +0 -54
  68. package/src/ui/components/ModelListView.js +0 -214
  69. package/src/ui/components/Notifications.js +0 -55
  70. package/src/ui/components/StreamingIndicator.js +0 -36
  71. package/src/ui/contexts/AppContext.js +0 -25
  72. package/src/ui/contexts/StreamingContext.js +0 -20
  73. package/src/ui/contexts/UIStateContext.js +0 -117
  74. package/src/ui/example-usage.js +0 -180
  75. package/src/ui/hooks/useTerminalResize.js +0 -39
  76. package/src/ui/themes/semantic-tokens.js +0 -73
  77. package/src/ui/utils/text-buffer.js +0 -975
  78. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_ssgManifest.js +0 -0
  81. /package/src/{ui → frontend}/components/BlankLine.js +0 -0
  82. /package/src/{ui → frontend}/components/FileDiffViewer.js +0 -0
  83. /package/src/{ui → frontend}/components/HelpView.js +0 -0
  84. /package/src/{ui → frontend}/hooks/useCompletion.js +0 -0
  85. /package/src/{ui → frontend}/hooks/useKeypress.js +0 -0
  86. /package/src/{ui → frontend}/utils/diffUtils.js +0 -0
  87. /package/src/{ui → frontend}/utils/renderInkComponent.js +0 -0
@@ -0,0 +1,81 @@
1
+ # UI System Documentation
2
+
3
+ 이 디렉토리는 Ink 기반의 터미널 UI 시스템을 포함합니다.
4
+
5
+ ## 구조
6
+
7
+ ```
8
+ ui/
9
+ ├── App.js # 메인 앱 진입점
10
+ ├── index.js # UI 렌더링 진입점
11
+ ├── components/ # UI 컴포넌트
12
+ │ ├── Header.js # 헤더 (로고)
13
+ │ ├── Footer.js # 푸터 (상태 정보)
14
+ │ ├── ConversationItem.js # 개별 대화 아이템
15
+ │ ├── Input.js # 입력 컴포넌트
16
+ │ ├── SessionSpinner.js # 세션 처리 스피너
17
+ │ ├── AutocompleteMenu.js # 자동완성 메뉴
18
+ │ ├── BlankLine.js # 빈 줄 컴포넌트
19
+ │ ├── FileDiffViewer.js # 파일 diff 뷰어
20
+ │ ├── ToolApprovalPrompt.js # 도구 승인 프롬프트
21
+ │ ├── SetupWizard.js # 초기 설정 마법사
22
+ │ ├── HelpView.js # 도움말 뷰
23
+ │ ├── CurrentModelView.js # 현재 모델 뷰
24
+ │ ├── ModelListView.js # 모델 목록 뷰
25
+ │ └── ModelUpdatedView.js # 모델 변경 확인 뷰
26
+ ├── hooks/ # Custom hooks
27
+ │ ├── useKeypress.js # 키보드 이벤트
28
+ │ └── useCompletion.js # 자동완성
29
+ ├── design/ # 디자인 시스템
30
+ │ └── themeColors.js # 테마 색상
31
+ └── utils/ # 유틸리티
32
+ ├── inputBuffer.js # 입력 버퍼 (멀티라인)
33
+ ├── outputRedirector.js # 출력 리다이렉터
34
+ ├── syntaxHighlighter.js # 문법 하이라이터
35
+ ├── markdownParser.js # 마크다운 파서
36
+ ├── diffUtils.js # Diff 유틸리티
37
+ └── renderInkComponent.js # Ink 컴포넌트 렌더러
38
+ ```
39
+
40
+ ## 아키텍처
41
+
42
+ ### 레이아웃 구조
43
+
44
+ ```
45
+ ┌──────────────────────────────────┐
46
+ │ App (Main Layout) │
47
+ │ ├─ Header (Static) │
48
+ │ ├─ History (Static) │
49
+ │ ├─ SessionSpinner (Dynamic) │
50
+ │ ├─ Input │
51
+ │ └─ Footer │
52
+ └──────────────────────────────────┘
53
+ ```
54
+
55
+ ## UI 상태 관리
56
+
57
+ UI 상태는 `uiEvents` 모듈을 통해 관리됩니다. 자세한 내용은 `src/system/ui_events.js`를 참조하세요.
58
+
59
+
60
+ ## 메시지 타입
61
+
62
+ | Type | Icon | Color | Usage |
63
+ |------|------|-------|-------|
64
+ | `user` | `>` | Accent | 사용자 입력 |
65
+ | `assistant` | `◆` | Info | AI 응답 |
66
+ | `system` | `ℹ` | Secondary | 시스템 메시지 |
67
+ | `error` | `✗` | Error | 에러 메시지 |
68
+ | `tool` | `⚙` | Success | 도구 실행 결과 |
69
+ | `thinking` | `💭` | Link | AI 사고 과정 |
70
+
71
+ ## 애니메이션
72
+
73
+ ### SessionSpinner
74
+ - ink-spinner 라이브러리 사용
75
+ - 경과 시간 표시 (초/분)
76
+
77
+ ## 성능 최적화
78
+
79
+ 1. **Static 컴포넌트**: 불변 히스토리는 Static으로 렌더링하여 리렌더링 방지
80
+ 2. **Dynamic 영역**: 진행 중인 작업만 동적으로 업데이트
81
+ 3. **조건부 렌더링**: 필요한 컴포넌트만 표시
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Suggestions display component for autocompletion
2
+ * Autocomplete menu component for command suggestions
3
3
  */
4
4
 
5
5
  import React from 'react';
6
6
  import { Box, Text } from 'ink';
7
- import { theme } from '../themes/semantic-tokens.js';
7
+ import { theme } from '../design/themeColors.js';
8
8
 
9
- export function SuggestionsDisplay({ suggestions = [], activeIndex = 0 }) {
9
+ export function AutocompleteMenu({ suggestions = [], activeIndex = 0 }) {
10
10
  if (suggestions.length === 0) {
11
11
  return null;
12
12
  }
@@ -1,13 +1,12 @@
1
1
  /**
2
- * HistoryItemDisplay - Renders individual history items
2
+ * ConversationItem - Renders individual conversation items
3
3
  */
4
4
 
5
5
  import React, { useState, useEffect } from 'react';
6
6
  import { Box, Text } from 'ink';
7
- import { theme } from '../themes/semantic-tokens.js';
8
- import { StreamingIndicator } from './StreamingIndicator.js';
9
- import { colorizeCode } from '../utils/codeColorizer.js';
10
- import { renderMarkdown } from '../utils/markdownRenderer.js';
7
+ import { theme } from '../design/themeColors.js';
8
+ import { colorizeCode } from '../utils/syntaxHighlighter.js';
9
+ import { renderMarkdown } from '../utils/markdownParser.js';
11
10
  import { FileDiffViewer } from './FileDiffViewer.js';
12
11
  import { getToolDisplayName, formatToolCall } from '../../system/tool_registry.js';
13
12
  import { getFileSnapshot } from '../../system/file_integrity.js';
@@ -18,12 +17,9 @@ import { createHash } from 'crypto';
18
17
  import { createDebugLogger } from '../../util/debug_log.js';
19
18
  import { prepareEditReplaceDiff } from '../utils/diffUtils.js';
20
19
 
21
- const debugLog = createDebugLogger('ui_components.log', 'HistoryItemDisplay');
20
+ const debugLog = createDebugLogger('ui_components.log', 'ConversationItem');
22
21
  const OLDSTRING_LOG_FILE = join(DEBUG_LOG_DIR, 'oldstring.txt');
23
22
 
24
- // 스트리밍 인디케이터 및 operations 렌더링 활성화 여부
25
- const RENDER_STREAMING_INDICATORS = false;
26
-
27
23
  // Evidence logging for old_string NOT FOUND issues
28
24
  function logOldStringEvidence(evidence) {
29
25
  try {
@@ -183,7 +179,7 @@ function StandardDisplay({ item, isPending, hasFollowingResult, nextItem, isLast
183
179
 
184
180
  debugLog('---------- StandardDisplay START ----------');
185
181
  debugLog(`type: ${type}, toolName: ${toolName || 'N/A'}`);
186
- debugLog(`text: ${text?.substring(0, 100) || 'N/A'}...`);
182
+ debugLog(`text: ${typeof text === 'string' ? text?.substring(0, 100) : JSON.stringify(text)?.substring(0, 100) || 'N/A'}...`);
187
183
  debugLog(`hasFollowingResult: ${hasFollowingResult}, isLastInBatch: ${isLastInBatch}`);
188
184
 
189
185
  // tool_start는 args를, tool_result는 toolInput을 사용
@@ -756,38 +752,21 @@ function StandardDisplay({ item, isPending, hasFollowingResult, nextItem, isLast
756
752
  React.createElement(Text, {
757
753
  color: theme.text.secondary
758
754
  }, ` ${formattedArgs}`)
759
- ),
760
-
761
- RENDER_STREAMING_INDICATORS && operations.length > 0 && React.createElement(Box, {
762
- flexDirection: "column",
763
- marginLeft: 2,
764
- marginTop: 1
765
- },
766
- operations.map((op, index) =>
767
- React.createElement(OperationDisplay, {
768
- key: op.id || index,
769
- operation: op,
770
- isPending
771
- })
772
- )
773
- ),
774
-
775
- RENDER_STREAMING_INDICATORS && isPending && item.isStreaming && React.createElement(StreamingIndicator, {
776
- message: item.streamingMessage || 'Processing...'
777
- })
755
+ )
778
756
  );
779
757
  }
780
758
 
781
- debugLog(`Rendering default display: icon="${config.icon}", text="${text?.substring(0, 50)}..."`);
759
+ debugLog(`Rendering default display: icon="${config.icon}", text="${typeof text === 'string' ? text?.substring(0, 50) : JSON.stringify(text)?.substring(0, 50) || 'N/A'}..."`);
782
760
  debugLog(`marginBottom: ${marginBottom}, marginTop: ${marginTop}`);
783
761
  debugLog(`operations count: ${operations.length}`);
784
762
  debugLog('---------- StandardDisplay END ----------');
785
763
 
786
764
  // Check if this is a denied tool result
787
765
  // Don't display tool_result when denied (already shown in tool_start)
788
- const isDeniedResult = type === 'tool_result' &&
789
- (text?.includes('User denied tool execution') ||
790
- text?.includes('User denied code execution'));
766
+ const textStr = typeof text === 'string' ? text : '';
767
+ const isDeniedResult = type === 'tool_result' &&
768
+ (textStr.includes('User denied tool execution') ||
769
+ textStr.includes('User denied code execution'));
791
770
 
792
771
  if (isDeniedResult) {
793
772
  // Don't display tool_result when denied (already shown in tool_start)
@@ -798,41 +777,41 @@ function StandardDisplay({ item, isPending, hasFollowingResult, nextItem, isLast
798
777
  const isErrorResult = type === 'tool_result' && originalResult?.operation_successful === false;
799
778
  const textColor = isErrorResult ? theme.status.error : theme.text.primary;
800
779
 
780
+ // text가 구조화된 포맷인지 확인 (format_tool_result에서 반환)
781
+ let textContent;
782
+ if (text && typeof text === 'object' && text.type === 'formatted' && Array.isArray(text.parts)) {
783
+ // 구조화된 parts를 렌더링
784
+ textContent = React.createElement(Box, { flexDirection: "row" },
785
+ ...text.parts.map((part, idx) =>
786
+ React.createElement(Text, {
787
+ key: idx,
788
+ color: part.style?.color || textColor,
789
+ bold: part.style?.bold || false
790
+ }, part.text)
791
+ )
792
+ );
793
+ } else {
794
+ // 일반 문자열
795
+ textContent = React.createElement(Text, {
796
+ color: textColor
797
+ }, text);
798
+ }
799
+
801
800
  return React.createElement(Box, { flexDirection: "column", marginBottom, marginTop, marginLeft: 2 },
802
801
  React.createElement(Box, { flexDirection: "row" },
803
802
  React.createElement(Text, { color: config.color, bold: config.bold }, config.icon),
804
803
  React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
805
- React.createElement(Text, {
806
- color: textColor
807
- }, text)
804
+ textContent
808
805
  )
809
- ),
810
-
811
- RENDER_STREAMING_INDICATORS && operations.length > 0 && React.createElement(Box, {
812
- flexDirection: "column",
813
- marginLeft: 2,
814
- marginTop: 1
815
- },
816
- operations.map((op, index) =>
817
- React.createElement(OperationDisplay, {
818
- key: op.id || index,
819
- operation: op,
820
- isPending
821
- })
822
- )
823
- ),
824
-
825
- RENDER_STREAMING_INDICATORS && isPending && item.isStreaming && React.createElement(StreamingIndicator, {
826
- message: item.streamingMessage || 'Processing...'
827
- })
806
+ )
828
807
  );
829
808
  }
830
809
 
831
- export function HistoryItemDisplay({ item, isPending = false, terminalWidth, nextItem, isLastInBatch = false }) {
832
- debugLog('========== HistoryItemDisplay RENDER ==========');
810
+ export function ConversationItem({ item, isPending = false, terminalWidth, nextItem, isLastInBatch = false }) {
811
+ debugLog('========== ConversationItem RENDER ==========');
833
812
  debugLog(`Item type: ${item.type}`);
834
813
  debugLog(`Item toolName: ${item.toolName || 'N/A'}`);
835
- debugLog(`Item text: ${item.text?.substring(0, 100) || 'N/A'}...`);
814
+ debugLog(`Item text: ${typeof item.text === 'string' ? item.text?.substring(0, 100) : JSON.stringify(item.text)?.substring(0, 100) || 'N/A'}...`);
836
815
  debugLog(`isPending: ${isPending}, isLastInBatch: ${isLastInBatch}`);
837
816
  debugLog(`nextItem: ${nextItem ? nextItem.type : 'null'}`);
838
817
 
@@ -856,34 +835,3 @@ export function HistoryItemDisplay({ item, isPending = false, terminalWidth, nex
856
835
 
857
836
  return result;
858
837
  }
859
-
860
- const OPERATION_STATUS = {
861
- running: { icon: '⟳', color: theme.status.info },
862
- completed: { icon: '✓', color: theme.status.success },
863
- error: { icon: '✗', color: theme.status.error },
864
- pending: { icon: '○', color: theme.text.secondary },
865
- default: { icon: '•', color: theme.text.secondary }
866
- };
867
-
868
- function getOperationStatus(status) {
869
- return OPERATION_STATUS[status] || OPERATION_STATUS.default;
870
- }
871
-
872
- function OperationDisplay({ operation, isPending }) {
873
- const { name, status, description, progress } = operation;
874
- const statusConfig = getOperationStatus(status);
875
-
876
- return React.createElement(Box, { flexDirection: "column", marginBottom: 0 },
877
- React.createElement(Box, { flexDirection: "row" },
878
- React.createElement(Text, { color: statusConfig.color }, `${statusConfig.icon} `),
879
- React.createElement(Text, { color: theme.text.primary }, name),
880
- description && React.createElement(Text, {
881
- color: theme.text.secondary,
882
- dimColor: true
883
- }, ` - ${description}`)
884
- ),
885
- progress && React.createElement(Box, { marginLeft: 2 },
886
- React.createElement(Text, { color: theme.text.secondary }, progress)
887
- )
888
- );
889
- }
@@ -4,17 +4,15 @@
4
4
 
5
5
  import React from 'react';
6
6
  import { Box, Text } from 'ink';
7
- import { theme } from '../themes/semantic-tokens.js';
7
+ import { theme } from '../design/themeColors.js';
8
8
 
9
9
  export function CurrentModelView({ provider, modelId, modelInfo }) {
10
10
  const providerColors = {
11
- openai: 'cyan',
12
- anthropic: 'magenta'
11
+ openai: 'cyan'
13
12
  };
14
13
 
15
14
  const providerEmojis = {
16
- openai: '🤖',
17
- anthropic: '🧠'
15
+ openai: '🤖'
18
16
  };
19
17
 
20
18
  return React.createElement(Box, {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React from 'react';
6
6
  import { Box, Text } from 'ink';
7
- import { theme } from '../themes/semantic-tokens.js';
7
+ import { theme } from '../design/themeColors.js';
8
8
  import { OPENAI_MODELS } from '../../config/openai_models.js';
9
9
 
10
10
  export function Footer({ model = 'gpt-4', reasoningEffort = null, cwd = process.cwd() }) {
@@ -18,10 +18,8 @@ export function Footer({ model = 'gpt-4', reasoningEffort = null, cwd = process.
18
18
  displayModel = `${model} (${reasoningEffort})`;
19
19
  }
20
20
 
21
- return React.createElement(Box, { justifyContent: "space-between", paddingX: 1, marginTop: 0 },
22
- React.createElement(Box, null,
23
- React.createElement(Text, { color: theme.text.link }, `CWD:${displayCwd}`)),
24
- React.createElement(Box, null,
25
- React.createElement(Text, { color: theme.text.accent }, displayModel))
21
+ return React.createElement(Box, { paddingX: 1, marginTop: 0, gap: 1 },
22
+ React.createElement(Text, { color: theme.brand.dark }, displayModel),
23
+ React.createElement(Text, { color: theme.brand.dark }, displayCwd)
26
24
  );
27
25
  }
@@ -5,7 +5,7 @@
5
5
  import React from 'react';
6
6
  import { Box, Text } from 'ink';
7
7
  import Gradient from 'ink-gradient';
8
- import { theme } from '../themes/semantic-tokens.js';
8
+ import { theme } from '../design/themeColors.js';
9
9
 
10
10
  // const ASCII_LOGO = `
11
11
  // d8888 8888888 8888888888 Y88b d88P 8888888888 888
@@ -27,10 +27,7 @@ export function Header({ version = '1.0.0' }) {
27
27
  by 코드깎는노인
28
28
  `.split('\n').map(line => line.trim()).join("\n");
29
29
  return React.createElement(Box, { flexDirection: "column", marginBottom: 1, marginLeft: 2 },
30
- theme.ui.gradient
31
- ? React.createElement(Gradient, { colors: theme.ui.gradient },
32
- React.createElement(Text, null, ASCII_LOGO))
33
- : React.createElement(Text, { color: theme.text.accent }, ASCII_LOGO),
30
+ React.createElement(Text, { color: theme.brand.light }, ASCII_LOGO),
34
31
  React.createElement(Box, { justifyContent: "flex-left" },
35
32
  React.createElement(Text, { color: theme.text.secondary }, `AIEXEcode v${version}`)
36
33
  )
@@ -1,26 +1,20 @@
1
1
  /**
2
- * Advanced InputPrompt component with multi-line support
2
+ * Advanced Input component with multi-line support
3
3
  */
4
4
 
5
5
  import React, { useCallback, useState, useRef, useEffect, useMemo } from 'react';
6
6
  import { Box, Text } from 'ink';
7
- import { theme } from '../themes/semantic-tokens.js';
7
+ import { theme } from '../design/themeColors.js';
8
8
  import { useKeypress, keyMatchers, Command } from '../hooks/useKeypress.js';
9
9
  import { useCompletion } from '../hooks/useCompletion.js';
10
- import { SuggestionsDisplay } from './SuggestionsDisplay.js';
11
- import { cpSlice, cpLen } from '../utils/text-buffer.js';
10
+ import { AutocompleteMenu } from './AutocompleteMenu.js';
11
+ import { cpSlice, cpLen } from '../utils/inputBuffer.js';
12
12
  import { uiEvents } from '../../system/ui_events.js';
13
13
  import chalk from 'chalk';
14
14
  import stringWidth from 'string-width';
15
15
 
16
- const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
17
-
18
- function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, commands = [], placeholder = ' Type your message...', focus = true, isSessionRunning = false }) {
19
- const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
20
- const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
16
+ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, commands = [], placeholder = ' What should I do?', focus = true, isSessionRunning = false }) {
21
17
  const [isExiting, setIsExiting] = useState(false);
22
- const ctrlCTimerRef = useRef(null);
23
- const ctrlDTimerRef = useRef(null);
24
18
 
25
19
  const completionRaw = useCompletion(buffer, commands);
26
20
 
@@ -35,28 +29,6 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
35
29
  ]);
36
30
 
37
31
 
38
- const handleExit = useCallback((
39
- pressedOnce,
40
- setPressedOnce,
41
- timerRef
42
- ) => {
43
- if (pressedOnce) {
44
- if (timerRef.current) {
45
- clearTimeout(timerRef.current);
46
- }
47
- // Always call onExit callback
48
- if (onExit) {
49
- onExit();
50
- }
51
- } else {
52
- setPressedOnce(true);
53
- timerRef.current = setTimeout(() => {
54
- setPressedOnce(false);
55
- timerRef.current = null;
56
- }, CTRL_EXIT_PROMPT_DURATION_MS);
57
- }
58
- }, [onExit]);
59
-
60
32
  const handleSubmitAndClear = useCallback((submittedValue) => {
61
33
  buffer.setText('');
62
34
  onSubmit(submittedValue);
@@ -86,7 +58,11 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
86
58
 
87
59
  if (keyMatchers[Command.EXIT](key)) {
88
60
  if (buffer.text.length > 0) return;
89
- handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
61
+ // Direct exit
62
+ setIsExiting(true);
63
+ if (onExit) {
64
+ onExit();
65
+ }
90
66
  return;
91
67
  }
92
68
 
@@ -244,23 +220,14 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
244
220
  buffer,
245
221
  onClearScreen,
246
222
  handleSubmitAndClear,
247
- ctrlCPressedOnce,
248
- ctrlDPressedOnce,
249
- handleExit,
250
223
  completion,
251
224
  isSessionRunning,
252
- isExiting
225
+ isExiting,
226
+ onExit
253
227
  ]);
254
228
 
255
229
  useKeypress(handleInput, { isActive: focus });
256
230
 
257
- useEffect(() => {
258
- return () => {
259
- if (ctrlCTimerRef.current) clearTimeout(ctrlCTimerRef.current);
260
- if (ctrlDTimerRef.current) clearTimeout(ctrlDTimerRef.current);
261
- };
262
- }, []);
263
-
264
231
  // Extract all needed values in one go to avoid multiple getter calls
265
232
  const linesToRender = useMemo(() => buffer.viewportVisualLines, [buffer.viewportVisualLines]);
266
233
  const [cursorVisualRowAbsolute, cursorVisualColAbsolute] = useMemo(() => buffer.visualCursor, [buffer.visualCursor]);
@@ -269,14 +236,9 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
269
236
  const bufferTextLength = useMemo(() => buffer.text.length, [buffer.text]);
270
237
 
271
238
  return React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
272
- ctrlCPressedOnce && React.createElement(Text, { color: theme.status.warning },
273
- 'Press Ctrl+C again to exit.'),
274
- ctrlDPressedOnce && React.createElement(Text, { color: theme.status.warning },
275
- 'Press Ctrl+D again to exit.'),
276
-
277
239
  React.createElement(Box, {
278
240
  borderStyle: "round",
279
- borderColor: focus ? theme.border.focused : theme.border.default,
241
+ borderColor: theme.brand.light,
280
242
  paddingX: 1,
281
243
  flexDirection: "row",
282
244
  alignItems: "flex-start",
@@ -285,7 +247,7 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
285
247
  flexShrink: 0 // Prevent input from being squeezed by other elements
286
248
  // Don't set explicit width - let Ink calculate it
287
249
  },
288
- React.createElement(Text, { color: theme.text.accent }, '> '),
250
+ React.createElement(Text, { color: theme.brand.light }, '> '),
289
251
  React.createElement(Box, { flexGrow: 1, flexDirection: "column" },
290
252
  bufferTextLength === 0 && placeholder
291
253
  ? (focus
@@ -316,7 +278,7 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
316
278
  )
317
279
  ),
318
280
 
319
- completion.showSuggestions && React.createElement(SuggestionsDisplay, {
281
+ completion.showSuggestions && React.createElement(AutocompleteMenu, {
320
282
  suggestions: completion.suggestions,
321
283
  activeIndex: completion.activeSuggestionIndex
322
284
  })
@@ -325,4 +287,4 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
325
287
 
326
288
  // Don't memoize - buffer state changes frequently and we need to react to it
327
289
  // The buffer object reference is stable (from useRef), but its internal state changes
328
- export const InputPrompt = InputPromptComponent;
290
+ export const Input = InputPromptComponent;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Model List View Component - Ink-based UI for displaying available models
3
+ */
4
+
5
+ import React from 'react';
6
+ import { Box, Text } from 'ink';
7
+ import { theme } from '../design/themeColors.js';
8
+
9
+ export function ModelListView({ openaiModels }) {
10
+ const sections = [
11
+ // Header
12
+ React.createElement(Box, {
13
+ key: 'header',
14
+ borderStyle: 'double',
15
+ borderColor: 'gray',
16
+ paddingX: 2,
17
+ paddingY: 0,
18
+ justifyContent: 'center'
19
+ },
20
+ React.createElement(Text, {
21
+ bold: true,
22
+ color: 'whiteBright'
23
+ }, 'Available AI Models')
24
+ ),
25
+
26
+ React.createElement(Text, { key: 'spacer1' }, null),
27
+
28
+ // OpenAI Section
29
+ React.createElement(Box, {
30
+ key: 'openai-section',
31
+ flexDirection: 'column',
32
+ borderStyle: 'round',
33
+ borderColor: 'gray',
34
+ paddingX: 2,
35
+ paddingY: 1
36
+ },
37
+ React.createElement(Box, { flexDirection: 'column' },
38
+ React.createElement(Text, {
39
+ bold: true,
40
+ color: 'cyan'
41
+ }, '🤖 OpenAI Models'),
42
+ React.createElement(Text, {
43
+ dimColor: true,
44
+ italic: true
45
+ }, 'GPT-5 Series (Latest generation, recommended)'),
46
+ React.createElement(Text, null)
47
+ ),
48
+
49
+ openaiModels.map((model, index) =>
50
+ React.createElement(Box, {
51
+ key: model.id,
52
+ flexDirection: 'column',
53
+ marginBottom: index < openaiModels.length - 1 ? 1 : 0
54
+ },
55
+ React.createElement(Box, { flexDirection: 'row', gap: 1 },
56
+ React.createElement(Text, { color: 'green', bold: true }, '•'),
57
+ React.createElement(Text, {
58
+ color: 'white',
59
+ bold: true
60
+ }, model.id)
61
+ ),
62
+ React.createElement(Box, {
63
+ flexDirection: 'column',
64
+ marginLeft: 2,
65
+ gap: 0
66
+ },
67
+ React.createElement(Text, {
68
+ color: theme.text.secondary
69
+ }, `${model.name} - ${model.description}`),
70
+ React.createElement(Text, {
71
+ dimColor: true,
72
+ italic: true
73
+ }, `💰 $${model.pricing.input}/$${model.pricing.output} (input/output per 1M tokens)`)
74
+ )
75
+ )
76
+ )
77
+ )
78
+ ];
79
+
80
+ sections.push(
81
+ React.createElement(Text, { key: 'spacer4' }, null),
82
+
83
+ // Footer
84
+ React.createElement(Box, {
85
+ key: 'footer',
86
+ flexDirection: 'column',
87
+ borderStyle: 'single',
88
+ borderColor: 'gray',
89
+ paddingX: 2,
90
+ paddingY: 1
91
+ },
92
+ React.createElement(Text, { bold: true }, '📖 Usage'),
93
+ React.createElement(Text, null, ' /model <model-id>'),
94
+ React.createElement(Text, { dimColor: true }, ' Example: /model gpt-5'),
95
+ React.createElement(Text, null),
96
+ React.createElement(Text, { bold: true }, '🔗 More Info'),
97
+ React.createElement(Text, { color: theme.text.link }, ' OpenAI: https://platform.openai.com/docs/pricing')
98
+ )
99
+ );
100
+
101
+ return React.createElement(Box, {
102
+ flexDirection: 'column',
103
+ paddingX: 2,
104
+ paddingY: 1
105
+ }, ...sections);
106
+ }
@@ -4,17 +4,15 @@
4
4
 
5
5
  import React from 'react';
6
6
  import { Box, Text } from 'ink';
7
- import { theme } from '../themes/semantic-tokens.js';
7
+ import { theme } from '../design/themeColors.js';
8
8
 
9
9
  export function ModelUpdatedView({ provider, modelId, modelInfo, settingsFile, warning }) {
10
10
  const providerColors = {
11
- openai: 'cyan',
12
- anthropic: 'magenta'
11
+ openai: 'cyan'
13
12
  };
14
13
 
15
14
  const providerEmojis = {
16
- openai: '🤖',
17
- anthropic: '🧠'
15
+ openai: '🤖'
18
16
  };
19
17
 
20
18
  return React.createElement(Box, {
@@ -6,7 +6,7 @@
6
6
  import React, { useState, useEffect, memo } from 'react';
7
7
  import { Box, Text } from 'ink';
8
8
  import Spinner from 'ink-spinner';
9
- import { theme } from '../themes/semantic-tokens.js';
9
+ import { theme } from '../design/themeColors.js';
10
10
 
11
11
  // Internal component that handles the animation
12
12
  const SpinnerContent = memo(function SpinnerContent({ message, elapsedSeconds }) {
@@ -26,11 +26,11 @@ const SpinnerContent = memo(function SpinnerContent({ message, elapsedSeconds })
26
26
  marginBottom: 0
27
27
  },
28
28
  React.createElement(Box, { marginRight: 1 },
29
- React.createElement(Text, { color: theme.text.accent },
29
+ React.createElement(Text, { color: theme.brand.light },
30
30
  React.createElement(Spinner, { type: 'dots' })
31
31
  )
32
32
  ),
33
- React.createElement(Text, { color: theme.text.accent }, message),
33
+ React.createElement(Text, { color: theme.brand.light }, message),
34
34
  React.createElement(Text, { color: theme.text.secondary },
35
35
  ` (${formatTime(elapsedSeconds)})`
36
36
  )