mistagent 0.1.19 → 0.1.21

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 (198) hide show
  1. package/dist/src/api/quota.d.ts +13 -0
  2. package/dist/src/api/quota.js +7 -0
  3. package/dist/src/components/App.d.ts +2 -0
  4. package/dist/src/components/App.js +23 -5
  5. package/dist/src/components/Header.js +74 -166
  6. package/dist/src/components/MainContent.js +3 -14
  7. package/dist/src/components/shared/MarkdownRenderer.js +2 -17
  8. package/dist/src/components/shared/TextInput.js +62 -3
  9. package/dist/src/hooks/useChat.js +23 -2
  10. package/dist/src/hooks/useSlashCommand.js +45 -0
  11. package/dist/src/main.js +63 -4
  12. package/dist/src/types/api.d.ts +4 -0
  13. package/dist/src/utils/config.d.ts +2 -0
  14. package/dist/src/utils/config.js +23 -0
  15. package/dist/src/utils/constants.d.ts +1 -1
  16. package/dist/src/utils/constants.js +1 -1
  17. package/dist/src/utils/markdown.d.ts +10 -0
  18. package/dist/src/utils/markdown.js +223 -0
  19. package/dist/src/utils/updateChecker.js +10 -4
  20. package/package.json +3 -2
  21. package/dist/index.d.ts.map +0 -1
  22. package/dist/index.js.map +0 -1
  23. package/dist/src/api/auth.d.ts.map +0 -1
  24. package/dist/src/api/auth.js.map +0 -1
  25. package/dist/src/api/chat.d.ts.map +0 -1
  26. package/dist/src/api/chat.js.map +0 -1
  27. package/dist/src/api/client.d.ts.map +0 -1
  28. package/dist/src/api/client.js.map +0 -1
  29. package/dist/src/api/models.d.ts.map +0 -1
  30. package/dist/src/api/models.js.map +0 -1
  31. package/dist/src/api/sessions.d.ts.map +0 -1
  32. package/dist/src/api/sessions.js.map +0 -1
  33. package/dist/src/api/skills.d.ts.map +0 -1
  34. package/dist/src/api/skills.js.map +0 -1
  35. package/dist/src/api/tools.d.ts.map +0 -1
  36. package/dist/src/api/tools.js.map +0 -1
  37. package/dist/src/api/tunnel.d.ts.map +0 -1
  38. package/dist/src/api/tunnel.js.map +0 -1
  39. package/dist/src/components/App.d.ts.map +0 -1
  40. package/dist/src/components/App.js.map +0 -1
  41. package/dist/src/components/AppLayout.d.ts.map +0 -1
  42. package/dist/src/components/AppLayout.js.map +0 -1
  43. package/dist/src/components/Composer.d.ts.map +0 -1
  44. package/dist/src/components/Composer.js.map +0 -1
  45. package/dist/src/components/Footer.d.ts.map +0 -1
  46. package/dist/src/components/Footer.js.map +0 -1
  47. package/dist/src/components/Header.d.ts.map +0 -1
  48. package/dist/src/components/Header.js.map +0 -1
  49. package/dist/src/components/HistoryItemDisplay.d.ts.map +0 -1
  50. package/dist/src/components/HistoryItemDisplay.js.map +0 -1
  51. package/dist/src/components/InputPrompt.d.ts.map +0 -1
  52. package/dist/src/components/InputPrompt.js.map +0 -1
  53. package/dist/src/components/LoadingIndicator.d.ts.map +0 -1
  54. package/dist/src/components/LoadingIndicator.js.map +0 -1
  55. package/dist/src/components/LoginPrompt.d.ts.map +0 -1
  56. package/dist/src/components/LoginPrompt.js.map +0 -1
  57. package/dist/src/components/MainContent.d.ts.map +0 -1
  58. package/dist/src/components/MainContent.js.map +0 -1
  59. package/dist/src/components/ModelPicker.d.ts.map +0 -1
  60. package/dist/src/components/ModelPicker.js.map +0 -1
  61. package/dist/src/components/SessionPicker.d.ts.map +0 -1
  62. package/dist/src/components/SessionPicker.js.map +0 -1
  63. package/dist/src/components/SuggestionsDisplay.d.ts.map +0 -1
  64. package/dist/src/components/SuggestionsDisplay.js.map +0 -1
  65. package/dist/src/components/ThemePicker.d.ts.map +0 -1
  66. package/dist/src/components/ThemePicker.js.map +0 -1
  67. package/dist/src/components/messages/AssistantMessage.d.ts.map +0 -1
  68. package/dist/src/components/messages/AssistantMessage.js.map +0 -1
  69. package/dist/src/components/messages/CommandResult.d.ts.map +0 -1
  70. package/dist/src/components/messages/CommandResult.js.map +0 -1
  71. package/dist/src/components/messages/ErrorMessage.d.ts.map +0 -1
  72. package/dist/src/components/messages/ErrorMessage.js.map +0 -1
  73. package/dist/src/components/messages/InfoMessage.d.ts.map +0 -1
  74. package/dist/src/components/messages/InfoMessage.js.map +0 -1
  75. package/dist/src/components/messages/ModelMessage.d.ts.map +0 -1
  76. package/dist/src/components/messages/ModelMessage.js.map +0 -1
  77. package/dist/src/components/messages/SessionMessage.d.ts.map +0 -1
  78. package/dist/src/components/messages/SessionMessage.js.map +0 -1
  79. package/dist/src/components/messages/ToolCallMessage.d.ts.map +0 -1
  80. package/dist/src/components/messages/ToolCallMessage.js.map +0 -1
  81. package/dist/src/components/messages/UserMessage.d.ts.map +0 -1
  82. package/dist/src/components/messages/UserMessage.js.map +0 -1
  83. package/dist/src/components/shared/HorizontalLine.d.ts.map +0 -1
  84. package/dist/src/components/shared/HorizontalLine.js.map +0 -1
  85. package/dist/src/components/shared/MarkdownRenderer.d.ts.map +0 -1
  86. package/dist/src/components/shared/MarkdownRenderer.js.map +0 -1
  87. package/dist/src/components/shared/Spinner.d.ts.map +0 -1
  88. package/dist/src/components/shared/Spinner.js.map +0 -1
  89. package/dist/src/components/shared/TextInput.d.ts.map +0 -1
  90. package/dist/src/components/shared/TextInput.js.map +0 -1
  91. package/dist/src/contexts/AppContext.d.ts.map +0 -1
  92. package/dist/src/contexts/AppContext.js.map +0 -1
  93. package/dist/src/contexts/ChatContext.d.ts.map +0 -1
  94. package/dist/src/contexts/ChatContext.js.map +0 -1
  95. package/dist/src/contexts/KeypressContext.d.ts.map +0 -1
  96. package/dist/src/contexts/KeypressContext.js.map +0 -1
  97. package/dist/src/contexts/ModelContext.d.ts.map +0 -1
  98. package/dist/src/contexts/ModelContext.js.map +0 -1
  99. package/dist/src/contexts/SessionContext.d.ts.map +0 -1
  100. package/dist/src/contexts/SessionContext.js.map +0 -1
  101. package/dist/src/contexts/UIContext.d.ts.map +0 -1
  102. package/dist/src/contexts/UIContext.js.map +0 -1
  103. package/dist/src/hooks/useChat.d.ts.map +0 -1
  104. package/dist/src/hooks/useChat.js.map +0 -1
  105. package/dist/src/hooks/useFileCompletion.d.ts.map +0 -1
  106. package/dist/src/hooks/useFileCompletion.js.map +0 -1
  107. package/dist/src/hooks/useInputHistory.d.ts.map +0 -1
  108. package/dist/src/hooks/useInputHistory.js.map +0 -1
  109. package/dist/src/hooks/useKeypress.d.ts.map +0 -1
  110. package/dist/src/hooks/useKeypress.js.map +0 -1
  111. package/dist/src/hooks/useLoadingIndicator.d.ts.map +0 -1
  112. package/dist/src/hooks/useLoadingIndicator.js.map +0 -1
  113. package/dist/src/hooks/usePasteBuffer.d.ts.map +0 -1
  114. package/dist/src/hooks/usePasteBuffer.js.map +0 -1
  115. package/dist/src/hooks/useSlashCommand.d.ts.map +0 -1
  116. package/dist/src/hooks/useSlashCommand.js.map +0 -1
  117. package/dist/src/hooks/useStdinInterceptor.d.ts.map +0 -1
  118. package/dist/src/hooks/useStdinInterceptor.js.map +0 -1
  119. package/dist/src/hooks/useSymbolCompletion.d.ts.map +0 -1
  120. package/dist/src/hooks/useSymbolCompletion.js.map +0 -1
  121. package/dist/src/hooks/useTextBuffer.d.ts.map +0 -1
  122. package/dist/src/hooks/useTextBuffer.js.map +0 -1
  123. package/dist/src/main.d.ts.map +0 -1
  124. package/dist/src/main.js.map +0 -1
  125. package/dist/src/tools/code-analyzer/config/ignore-service.d.ts.map +0 -1
  126. package/dist/src/tools/code-analyzer/config/ignore-service.js.map +0 -1
  127. package/dist/src/tools/code-analyzer/config/supported-languages.d.ts.map +0 -1
  128. package/dist/src/tools/code-analyzer/config/supported-languages.js.map +0 -1
  129. package/dist/src/tools/code-analyzer/core/graph/graph.d.ts.map +0 -1
  130. package/dist/src/tools/code-analyzer/core/graph/graph.js.map +0 -1
  131. package/dist/src/tools/code-analyzer/core/graph/types.d.ts.map +0 -1
  132. package/dist/src/tools/code-analyzer/core/graph/types.js.map +0 -1
  133. package/dist/src/tools/code-analyzer/core/ingestion/ast-cache.d.ts.map +0 -1
  134. package/dist/src/tools/code-analyzer/core/ingestion/ast-cache.js.map +0 -1
  135. package/dist/src/tools/code-analyzer/core/ingestion/call-processor.d.ts.map +0 -1
  136. package/dist/src/tools/code-analyzer/core/ingestion/call-processor.js.map +0 -1
  137. package/dist/src/tools/code-analyzer/core/ingestion/community-processor.d.ts.map +0 -1
  138. package/dist/src/tools/code-analyzer/core/ingestion/community-processor.js.map +0 -1
  139. package/dist/src/tools/code-analyzer/core/ingestion/entry-point-scoring.d.ts.map +0 -1
  140. package/dist/src/tools/code-analyzer/core/ingestion/entry-point-scoring.js.map +0 -1
  141. package/dist/src/tools/code-analyzer/core/ingestion/filesystem-walker.d.ts.map +0 -1
  142. package/dist/src/tools/code-analyzer/core/ingestion/filesystem-walker.js.map +0 -1
  143. package/dist/src/tools/code-analyzer/core/ingestion/framework-detection.d.ts.map +0 -1
  144. package/dist/src/tools/code-analyzer/core/ingestion/framework-detection.js.map +0 -1
  145. package/dist/src/tools/code-analyzer/core/ingestion/heritage-processor.d.ts.map +0 -1
  146. package/dist/src/tools/code-analyzer/core/ingestion/heritage-processor.js.map +0 -1
  147. package/dist/src/tools/code-analyzer/core/ingestion/import-processor.d.ts.map +0 -1
  148. package/dist/src/tools/code-analyzer/core/ingestion/import-processor.js.map +0 -1
  149. package/dist/src/tools/code-analyzer/core/ingestion/parsing-processor.d.ts.map +0 -1
  150. package/dist/src/tools/code-analyzer/core/ingestion/parsing-processor.js.map +0 -1
  151. package/dist/src/tools/code-analyzer/core/ingestion/pipeline.d.ts.map +0 -1
  152. package/dist/src/tools/code-analyzer/core/ingestion/pipeline.js.map +0 -1
  153. package/dist/src/tools/code-analyzer/core/ingestion/process-processor.d.ts.map +0 -1
  154. package/dist/src/tools/code-analyzer/core/ingestion/process-processor.js.map +0 -1
  155. package/dist/src/tools/code-analyzer/core/ingestion/structure-processor.d.ts.map +0 -1
  156. package/dist/src/tools/code-analyzer/core/ingestion/structure-processor.js.map +0 -1
  157. package/dist/src/tools/code-analyzer/core/ingestion/symbol-table.d.ts.map +0 -1
  158. package/dist/src/tools/code-analyzer/core/ingestion/symbol-table.js.map +0 -1
  159. package/dist/src/tools/code-analyzer/core/ingestion/tree-sitter-queries.d.ts.map +0 -1
  160. package/dist/src/tools/code-analyzer/core/ingestion/tree-sitter-queries.js.map +0 -1
  161. package/dist/src/tools/code-analyzer/core/ingestion/utils.d.ts.map +0 -1
  162. package/dist/src/tools/code-analyzer/core/ingestion/utils.js.map +0 -1
  163. package/dist/src/tools/code-analyzer/core/ingestion/workers/parse-worker.d.ts.map +0 -1
  164. package/dist/src/tools/code-analyzer/core/ingestion/workers/parse-worker.js.map +0 -1
  165. package/dist/src/tools/code-analyzer/core/ingestion/workers/worker-pool.d.ts.map +0 -1
  166. package/dist/src/tools/code-analyzer/core/ingestion/workers/worker-pool.js.map +0 -1
  167. package/dist/src/tools/code-analyzer/core/tree-sitter/parser-loader.d.ts.map +0 -1
  168. package/dist/src/tools/code-analyzer/core/tree-sitter/parser-loader.js.map +0 -1
  169. package/dist/src/tools/code-analyzer/index.d.ts.map +0 -1
  170. package/dist/src/tools/code-analyzer/index.js.map +0 -1
  171. package/dist/src/tools/code-analyzer/lib/utils.d.ts.map +0 -1
  172. package/dist/src/tools/code-analyzer/lib/utils.js.map +0 -1
  173. package/dist/src/tools/code-analyzer/types/pipeline.d.ts.map +0 -1
  174. package/dist/src/tools/code-analyzer/types/pipeline.js.map +0 -1
  175. package/dist/src/types/api.d.ts.map +0 -1
  176. package/dist/src/types/api.js.map +0 -1
  177. package/dist/src/types/history.d.ts.map +0 -1
  178. package/dist/src/types/history.js.map +0 -1
  179. package/dist/src/utils/colors.d.ts.map +0 -1
  180. package/dist/src/utils/colors.js.map +0 -1
  181. package/dist/src/utils/config.d.ts.map +0 -1
  182. package/dist/src/utils/config.js.map +0 -1
  183. package/dist/src/utils/constants.d.ts.map +0 -1
  184. package/dist/src/utils/constants.js.map +0 -1
  185. package/dist/src/utils/fileRef.d.ts.map +0 -1
  186. package/dist/src/utils/fileRef.js.map +0 -1
  187. package/dist/src/utils/fileTunnel.d.ts.map +0 -1
  188. package/dist/src/utils/fileTunnel.js.map +0 -1
  189. package/dist/src/utils/formatters.d.ts.map +0 -1
  190. package/dist/src/utils/formatters.js.map +0 -1
  191. package/dist/src/utils/pasteUtils.d.ts.map +0 -1
  192. package/dist/src/utils/pasteUtils.js.map +0 -1
  193. package/dist/src/utils/skillScanner.d.ts.map +0 -1
  194. package/dist/src/utils/skillScanner.js.map +0 -1
  195. package/dist/src/utils/textUtils.d.ts.map +0 -1
  196. package/dist/src/utils/textUtils.js.map +0 -1
  197. package/dist/src/utils/updateChecker.d.ts.map +0 -1
  198. package/dist/src/utils/updateChecker.js.map +0 -1
@@ -0,0 +1,13 @@
1
+ export interface QuotaBalance {
2
+ usd_balance: number;
3
+ usd_total_granted: number;
4
+ usd_total_consumed: number;
5
+ usage_today_tokens: number;
6
+ usage_this_month_tokens: number;
7
+ cost_today_usd: number;
8
+ cost_this_month_usd: number;
9
+ }
10
+ export declare const quotaApi: {
11
+ balance(): Promise<QuotaBalance>;
12
+ };
13
+ //# sourceMappingURL=quota.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { getClient } from './client.js';
2
+ export const quotaApi = {
3
+ balance() {
4
+ return getClient().get('/api/v1/quota/balance');
5
+ },
6
+ };
7
+ //# sourceMappingURL=quota.js.map
@@ -18,6 +18,8 @@ interface AppProps {
18
18
  loginError: string | null;
19
19
  isAuthenticated: boolean;
20
20
  initialTheme: ThemeMode;
21
+ terminalWidth: number;
22
+ terminalHeight: number;
21
23
  }
22
24
  export declare const App: React.FC<AppProps>;
23
25
  export {};
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useCallback } from 'react';
2
+ import { useState, useCallback, useEffect } from 'react';
3
+ import { useStdout } from 'ink';
3
4
  import { AppStateContext, AppActionsContext, } from '../contexts/AppContext.js';
4
5
  import { ChatStateContext, ChatDispatchContext, useChatReducer, } from '../contexts/ChatContext.js';
5
6
  import { UIStateContext, UIActionsContext, } from '../contexts/UIContext.js';
@@ -30,15 +31,32 @@ const AppInner = ({ onLogin, loginError, isAuthenticated, authEnabled }) => {
30
31
  }
31
32
  return _jsx(AppLayout, { onSubmit: handleSubmit, onCancel: cancelStream, onConfirmPlan: confirmPlan, onResolveToolApproval: resolveToolApproval });
32
33
  };
33
- export const App = ({ serverUrl, token, username, authEnabled, version, healthData, availableCommands, availableTools, initialModels, initialCurrentModel, initialSessions, onLogin, onLogout, loginError, isAuthenticated, initialTheme, }) => {
34
+ export const App = ({ serverUrl, token, username, authEnabled, version, healthData, availableCommands, availableTools, initialModels, initialCurrentModel, initialSessions, onLogin, onLogout, loginError, isAuthenticated, initialTheme, terminalWidth: initialTerminalWidth, terminalHeight: initialTerminalHeight, }) => {
35
+ const { stdout } = useStdout();
34
36
  const [appToken, setAppToken] = useState(token);
35
37
  const [appUsername, setAppUsername] = useState(username);
36
38
  const [chatState, chatDispatch] = useChatReducer();
37
- // Terminal size
38
- const [terminalWidth] = useState(process.stdout.columns || 80);
39
- const [terminalHeight] = useState(process.stdout.rows || 24);
40
39
  const [activeDialog, setActiveDialog] = useState('none');
41
40
  const [theme, setThemeState] = useState(initialTheme);
41
+ // Track terminal size — updated by both:
42
+ // 1. stdout 'resize' events (primary: fired by the OS → Node TTY → Ink's stdout)
43
+ // 2. A polling fallback every 200ms (catches any missed events)
44
+ const [terminalWidth, setTerminalWidth] = useState(() => stdout.columns || process.stdout.columns || initialTerminalWidth);
45
+ const [terminalHeight, setTerminalHeight] = useState(() => stdout.rows || process.stdout.rows || initialTerminalHeight);
46
+ useEffect(() => {
47
+ const readSize = () => {
48
+ setTerminalWidth(stdout.columns || process.stdout.columns || 80);
49
+ setTerminalHeight(stdout.rows || process.stdout.rows || 24);
50
+ };
51
+ stdout.on('resize', readSize);
52
+ process.stdout.on('resize', readSize);
53
+ const poll = setInterval(readSize, 200);
54
+ return () => {
55
+ stdout.off('resize', readSize);
56
+ process.stdout.off('resize', readSize);
57
+ clearInterval(poll);
58
+ };
59
+ }, [stdout]);
42
60
  const appState = {
43
61
  serverUrl,
44
62
  token: appToken,
@@ -8,12 +8,7 @@ import { useSessionState } from '../contexts/SessionContext.js';
8
8
  import { useChatState } from '../contexts/ChatContext.js';
9
9
  import { useAppState } from '../contexts/AppContext.js';
10
10
  import { MASCOT_LINES, VERSION, TIPS, getMascotGradient } from '../utils/constants.js';
11
- // ── Catppuccin tokens (matching pencil variables) ──
12
11
  const C = {
13
- bg: '#1E1E2E',
14
- bgDark: '#181825',
15
- bgDeep: '#11111B',
16
- surface1: '#313244',
17
12
  border: '#585B70',
18
13
  dimtext: '#6C7086',
19
14
  subtext: '#A6ADC8',
@@ -24,185 +19,98 @@ const C = {
24
19
  teal: '#94E2D5',
25
20
  green: '#A6E3A1',
26
21
  red: '#F38BA8',
27
- peach: '#FAB387',
28
- rose: '#E05A4E',
29
22
  yellow: '#F9E2AF',
30
23
  };
31
- // ── Shortcuts (matching pencil: full key names) ──
32
- const SHORTCUTS = [
33
- { key: '/', desc: '命令' },
34
- { key: '@', desc: '文件' },
35
- { key: '@@', desc: '符号' },
36
- { key: 'Shift+Tab', desc: '模式' },
37
- { key: 'Shift+Enter', desc: '多行' },
38
- ];
39
- // ── Helper functions ──
40
- const pad = (s, width) => {
41
- const len = stringWidth(s);
42
- if (len >= width)
43
- return s;
44
- return s + ' '.repeat(width - len);
45
- };
46
- const centerPad = (s, width) => {
47
- const len = stringWidth(s);
48
- if (len >= width)
49
- return s;
50
- const left = Math.floor((width - len) / 2);
51
- const right = width - len - left;
52
- return ' '.repeat(left) + s + ' '.repeat(right);
53
- };
54
- const truncate = (s, maxLen) => {
55
- if (maxLen <= 0)
24
+ // Three-tier breakpoints
25
+ // col1(mascot)=35 + div=1 + col2(stats)=26 + div=1 + col3(min~10) + border+pad~6 = ~79 minimum
26
+ // Use 90 as safe threshold so the 3-column layout never gets squeezed
27
+ const CONDENSED_MAX = 40; // <40 → single line
28
+ const COMPACT_MAX = 90; // <90 → stacked compact
29
+ // ≥90 → full 3-column
30
+ function truncate(s, max) {
31
+ if (max <= 0)
56
32
  return '';
57
- if (stringWidth(s) <= maxLen)
33
+ if (stringWidth(s) <= max)
58
34
  return s;
59
- let result = '';
35
+ let out = '';
60
36
  for (const ch of s) {
61
- if (stringWidth(result + ch) > maxLen - 1)
37
+ if (stringWidth(out + ch) > max - 1)
62
38
  break;
63
- result += ch;
39
+ out += ch;
64
40
  }
65
- return result + '…';
41
+ return out + '…';
42
+ }
43
+ // ─────────────────────────────────────────────
44
+ // Tier 1 — Condensed (<40 cols)
45
+ // Single row: ❯ MIST v0.x model ●
46
+ // ─────────────────────────────────────────────
47
+ const CondensedHeader = ({ modelName, isConnected, version }) => (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: C.blue, bold: true, children: '❯' }), _jsx(Text, { color: C.text, bold: true, children: 'MIST' }), _jsx(Text, { color: C.dimtext, children: `v${version}` }), _jsx(Text, { color: C.sapphire, children: modelName }), _jsx(Text, { color: isConnected ? C.green : C.red, children: '●' })] }));
48
+ // ─────────────────────────────────────────────
49
+ // Tier 2 — Compact (40–69 cols)
50
+ // Stacked: brand + model + status + shortcuts
51
+ // ─────────────────────────────────────────────
52
+ const CompactHeader = ({ modelName, isConnected, modeLabel, theme, version }) => {
53
+ const themeIcon = theme === 'dark' ? '◐' : '◑';
54
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: C.border, paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: C.blue, bold: true, children: `❯ MIST v${version}` }), _jsx(Text, { color: isConnected ? C.green : C.red, children: isConnected ? '● ON' : '○ OFF' })] }), _jsx(Text, { color: C.sapphire, children: `model: ${modelName}` }), _jsx(Text, { color: C.dimtext, children: `mode: ${modeLabel} ${themeIcon} ${theme}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: C.dimtext, children: '[/] cmd [@] file [Shift+Tab] mode' }) })] }));
55
+ };
56
+ // ─────────────────────────────────────────────
57
+ // Tier 3 — Horizontal (≥70 cols)
58
+ // Full 3-column with Ink flexbox (no manual widths)
59
+ // ─────────────────────────────────────────────
60
+ const MascotLogo = () => {
61
+ const gradient = getMascotGradient();
62
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [MASCOT_LINES.map((line, i) => (_jsx(Text, { color: gradient[i] ?? gradient[gradient.length - 1], bold: true, children: line }, i))), _jsx(Text, { color: C.dimtext, children: 'A G E N T' })] }));
66
63
  };
67
- // ── Border chars (dashed style) ──
68
- const B = {
69
- topLeft: '╭',
70
- topRight: '',
71
- bottomLeft: '',
72
- bottomRight: '',
73
- h: '', // dashed horizontal
74
- v: '┊', // dashed vertical
64
+ const StatsPanel = ({ modelName, sessionCount, modeLabel, theme }) => {
65
+ const themeIcon = theme === 'dark' ? '◐' : '◑';
66
+ const rows = [
67
+ { label: 'model', value: truncate(modelName, 18), color: C.sapphire },
68
+ { label: 'sessions', value: String(sessionCount), color: C.teal },
69
+ { label: 'mode', value: modeLabel, color: modeLabel === 'plan' ? C.mauve : C.green },
70
+ { label: 'theme', value: `${themeIcon} ${theme}`, color: C.mauve },
71
+ ];
72
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: C.subtext, children: '// stats' }), rows.map(r => (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Box, { width: 10, children: _jsx(Text, { color: C.dimtext, children: r.label }) }), _jsx(Text, { color: r.color, bold: true, children: r.value })] }, r.label)))] }));
73
+ };
74
+ const ActivityPanel = ({ tip, sessions }) => {
75
+ const dotColors = [C.green, C.blue, C.mauve];
76
+ const recent = sessions.slice(0, 3);
77
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: C.subtext, children: '// tip' }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: C.green, bold: true, children: '$ ' }), _jsx(Text, { color: C.text, children: tip })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: C.subtext, children: `// recent (${sessions.length})` }) }), recent.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: C.dimtext, children: 'No recent activity' }) })) : (recent.map((s, i) => {
78
+ const d = new Date(s.last_activity);
79
+ const t = d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
80
+ return (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: C.dimtext, children: `${t} ` }), _jsx(Text, { color: dotColors[i % dotColors.length], children: '● ' }), _jsx(Text, { color: C.subtext, children: truncate(s.title || 'New Chat', 25) })] }, i));
81
+ }))] }));
75
82
  };
76
- // ── Dashed divider (orange, matching outer border) ──
77
- const DashedDivider = ({ width }) => (_jsx(Text, { color: C.yellow, children: B.h.repeat(width) }));
78
- // ── Empty row (full inner width, bordered) ──
79
- const EmptyRow = ({ width }) => (_jsxs(Text, { children: [_jsx(Text, { color: C.yellow, children: B.v }), _jsx(Text, { children: ' '.repeat(width) }), _jsx(Text, { color: C.yellow, children: B.v })] }));
83
+ const SHORTCUTS = [
84
+ { key: '/', desc: '命令' },
85
+ { key: '@', desc: '文件' },
86
+ { key: '@@', desc: '符号' },
87
+ { key: 'Shift+Tab', desc: '模式' },
88
+ { key: 'Shift+↵', desc: '多行' },
89
+ ];
90
+ const HorizontalHeader = ({ modelName, isConnected, sessionCount, modeLabel, theme, tip, sessions, version }) => {
91
+ const netColor = isConnected ? C.green : C.red;
92
+ const netBadge = isConnected ? '● CONNECTED' : '○ OFFLINE';
93
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: C.border, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: C.blue, bold: true, children: '❯ ' }), _jsx(Text, { color: C.text, bold: true, children: 'MIST AGENT ' }), _jsx(Text, { color: C.mauve, bold: true, children: `[v${version}]` })] }), _jsx(Text, { color: netColor, children: `[ ${netBadge} ]` })] }), _jsx(Box, { borderStyle: "classic", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: C.border }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, paddingX: 2, paddingY: 1, children: _jsx(MascotLogo, {}) }), _jsx(Box, { borderStyle: "classic", borderLeft: true, borderTop: false, borderBottom: false, borderRight: false, borderColor: C.border }), _jsx(Box, { flexShrink: 0, width: 26, paddingX: 1, paddingY: 1, children: _jsx(StatsPanel, { modelName: modelName, sessionCount: sessionCount, modeLabel: modeLabel, theme: theme }) }), _jsx(Box, { borderStyle: "classic", borderLeft: true, borderTop: false, borderBottom: false, borderRight: false, borderColor: C.border }), _jsx(Box, { flexGrow: 1, flexShrink: 1, paddingX: 1, paddingY: 1, overflow: "hidden", children: _jsx(ActivityPanel, { tip: tip, sessions: sessions }) })] }), _jsx(Box, { borderStyle: "classic", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: C.border }), _jsx(Box, { flexDirection: "row", justifyContent: "center", gap: 3, paddingX: 1, paddingY: 1, children: SHORTCUTS.map(s => (_jsxs(Text, { color: C.dimtext, children: [_jsx(Text, { color: C.subtext, children: `[${s.key}]` }), ` ${s.desc}`] }, s.key))) })] }));
94
+ };
95
+ // ─────────────────────────────────────────────
96
+ // Root export — picks tier based on terminalWidth
97
+ // ─────────────────────────────────────────────
80
98
  export const Header = () => {
81
99
  const { currentModel } = useModelState();
82
100
  const { terminalWidth, theme } = useUIState();
83
101
  const { sessions } = useSessionState();
84
102
  const { forceMode } = useChatState();
85
103
  const { healthData } = useAppState();
86
- const modelName = currentModel?.model ?? 'MistAgent 0.1';
104
+ const modelName = currentModel?.model ?? 'MistAgent';
87
105
  const isConnected = !!healthData;
88
- const innerWidth = terminalWidth - 2;
89
- // ── Column widths ──
90
- const mascotWidth = Math.max(...MASCOT_LINES.map(l => stringWidth(l)));
91
- const logoColWidth = mascotWidth + 8;
92
- const statsColWidth = 28;
93
- const infoColWidth = Math.max(10, innerWidth - logoColWidth - statsColWidth - 2);
94
- // ── Random tip (stable per mount) ──
95
- const [tipText] = React.useState(() => TIPS[Math.floor(Math.random() * TIPS.length)]);
96
- const mascotGradient = getMascotGradient();
97
- // ── Stats data ──
98
106
  const modeLabel = forceMode === 'plan' ? 'plan' : 'chat';
99
- const themeIcon = theme === 'dark' ? '◐' : '◑';
100
- const statsData = [
101
- { label: 'model', value: modelName, valueColor: C.sapphire },
102
- { label: 'sessions', value: String(sessions.length), valueColor: C.teal },
103
- { label: 'mode', value: modeLabel, valueColor: forceMode === 'plan' ? C.mauve : C.green },
104
- { label: 'theme', value: `${themeIcon} ${theme}`, valueColor: C.mauve },
105
- ];
106
- // ── Recent sessions ──
107
- const recentSessions = sessions.slice(0, 3);
108
- const dotColors = [C.green, C.blue, C.mauve];
109
- const emptyLogo = () => _jsx(Text, { children: ' '.repeat(logoColWidth) });
110
- const emptyStats = () => _jsx(Text, { children: ' '.repeat(statsColWidth) });
111
- const emptyInfo = () => _jsx(Text, { children: ' '.repeat(infoColWidth) });
112
- // Col 1: Logo — top padding(2) + mascot(6) + gap + subtitle + bottom padding(2)
113
- const logoRows = [
114
- emptyLogo, // top padding 1
115
- emptyLogo, // top padding 2
116
- ...MASCOT_LINES.map((line, i) => () => {
117
- const color = mascotGradient[i] ?? mascotGradient[mascotGradient.length - 1];
118
- return _jsx(Text, { color: color, bold: true, children: centerPad(line, logoColWidth) });
119
- }),
120
- emptyLogo, // gap before subtitle
121
- () => _jsx(Text, { color: C.dimtext, children: centerPad('A G E N T', logoColWidth) }),
122
- emptyLogo, // bottom padding 1
123
- emptyLogo, // bottom padding 2
124
- ];
125
- // Col 2: Stats — top padding(2) + header + gap + 4 rows (with gaps) + bottom padding
126
- const statsRows = [
127
- emptyStats, // top padding 1
128
- emptyStats, // top padding 2
129
- () => _jsx(Text, { color: C.subtext, children: pad(' // quick stats', statsColWidth) }),
130
- emptyStats, // gap after header
131
- ...statsData.flatMap((s, i) => {
132
- const row = () => {
133
- const lbl = ` ${s.label}`;
134
- const val = `${s.value} `;
135
- const gap = Math.max(1, statsColWidth - stringWidth(lbl) - stringWidth(val));
136
- return (_jsxs(Text, { children: [_jsx(Text, { color: C.dimtext, children: lbl }), _jsx(Text, { children: ' '.repeat(gap) }), _jsx(Text, { color: s.valueColor, bold: true, children: val })] }));
137
- };
138
- // Add empty row between stat rows (not after the last one)
139
- return i < statsData.length - 1 ? [row, emptyStats] : [row];
140
- }),
141
- emptyStats, // bottom padding 1
142
- emptyStats, // bottom padding 2
143
- ];
144
- // Col 3: Info — top padding(2) + tip section + gap + recent section + bottom padding
145
- const infoRows = [
146
- emptyInfo, // top padding
147
- () => _jsx(Text, { color: C.subtext, children: pad(' // daily tip', infoColWidth) }),
148
- emptyInfo, // gap after tip label
149
- () => {
150
- const tipPrefix = ' $ ';
151
- const tipContent = truncate(tipText, infoColWidth - stringWidth(tipPrefix));
152
- return (_jsxs(Text, { children: [_jsx(Text, { color: C.green, bold: true, children: tipPrefix }), _jsx(Text, { color: C.text, children: pad(tipContent, infoColWidth - stringWidth(tipPrefix)) })] }));
153
- },
154
- emptyInfo, // gap between sections
155
- emptyInfo,
156
- () => _jsx(Text, { color: C.subtext, children: pad(' // recent activity (' + sessions.length + ')', infoColWidth) }),
157
- emptyInfo, // gap after recent label
158
- ];
159
- if (recentSessions.length > 0) {
160
- for (const [idx, s] of recentSessions.entries()) {
161
- const dotColor = dotColors[idx % dotColors.length];
162
- infoRows.push(() => {
163
- const date = new Date(s.last_activity);
164
- const timeStr = date.toLocaleString('zh-CN', {
165
- month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
166
- });
167
- const title = s.title || 'New Chat';
168
- const timePrefix = ` ${timeStr} `;
169
- const dotStr = '● ';
170
- const usedWidth = stringWidth(timePrefix) + stringWidth(dotStr);
171
- const titleWidth = Math.max(0, infoColWidth - usedWidth);
172
- const displayTitle = pad(truncate(title, titleWidth), titleWidth);
173
- return (_jsxs(Text, { children: [_jsx(Text, { color: C.dimtext, children: timePrefix }), _jsx(Text, { color: dotColor, children: dotStr }), _jsx(Text, { color: C.subtext, children: displayTitle })] }));
174
- });
175
- // Add empty row between recent items (not after the last one)
176
- if (idx < recentSessions.length - 1) {
177
- infoRows.push(emptyInfo);
178
- }
179
- }
107
+ const [tip] = React.useState(() => TIPS[Math.floor(Math.random() * TIPS.length)]);
108
+ if (terminalWidth < CONDENSED_MAX) {
109
+ return _jsx(CondensedHeader, { modelName: modelName, isConnected: isConnected, version: VERSION });
180
110
  }
181
- else {
182
- infoRows.push(() => _jsx(Text, { color: C.dimtext, children: pad(' No recent activity', infoColWidth) }));
111
+ if (terminalWidth < COMPACT_MAX) {
112
+ return (_jsx(CompactHeader, { modelName: modelName, isConnected: isConnected, modeLabel: modeLabel, theme: theme, version: VERSION }));
183
113
  }
184
- // Equalize row count
185
- const maxRows = Math.max(logoRows.length, statsRows.length, infoRows.length);
186
- while (logoRows.length < maxRows)
187
- logoRows.push(emptyLogo);
188
- while (statsRows.length < maxRows)
189
- statsRows.push(emptyStats);
190
- while (infoRows.length < maxRows)
191
- infoRows.push(emptyInfo);
192
- // ── Top bar layout ──
193
- const netColor = isConnected ? C.green : C.red;
194
- return (_jsxs(Box, { flexDirection: "column", width: terminalWidth, marginTop: 1, children: [_jsx(Text, { children: _jsxs(Text, { color: C.yellow, children: [B.topLeft, B.h.repeat(innerWidth), B.topRight] }) }), _jsx(EmptyRow, { width: innerWidth }), (() => {
195
- const leftPad = ' ';
196
- const prompt = '❯';
197
- const brand = ' M I S T A G E N T ';
198
- const verBadge = `[ v${VERSION} ]`;
199
- const netBadge = isConnected ? '[ ● CONNECTED ]' : '[ ○ OFFLINE ]';
200
- const leftStr = `${leftPad}${prompt}${brand}${verBadge}`;
201
- const rightStr = `${netBadge} `;
202
- const gap = Math.max(1, innerWidth - stringWidth(leftStr) - stringWidth(rightStr));
203
- return (_jsxs(Text, { children: [_jsx(Text, { color: C.yellow, children: B.v }), _jsx(Text, { children: leftPad }), _jsx(Text, { color: C.blue, bold: true, children: prompt }), _jsx(Text, { color: C.text, bold: true, children: brand }), _jsx(Text, { color: C.mauve, bold: true, children: verBadge }), _jsx(Text, { children: ' '.repeat(gap) }), _jsx(Text, { color: netColor, children: netBadge }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: C.yellow, children: B.v })] }));
204
- })(), _jsx(EmptyRow, { width: innerWidth }), _jsxs(Text, { children: [_jsx(Text, { color: C.yellow, children: B.v }), _jsx(DashedDivider, { width: innerWidth }), _jsx(Text, { color: C.yellow, children: B.v })] }), Array.from({ length: maxRows }).map((_, i) => {
205
- return (_jsxs(Text, { children: [_jsx(Text, { color: C.yellow, children: B.v }), logoRows[i](), _jsx(Text, { color: C.yellow, children: "\u2502" }), statsRows[i](), _jsx(Text, { color: C.yellow, children: "\u2502" }), infoRows[i](), _jsx(Text, { color: C.yellow, children: B.v })] }, i));
206
- }), _jsxs(Text, { children: [_jsx(Text, { color: C.yellow, children: B.v }), _jsx(DashedDivider, { width: innerWidth }), _jsx(Text, { color: C.yellow, children: B.v })] }), _jsx(EmptyRow, { width: innerWidth }), _jsxs(Text, { children: [_jsx(Text, { color: C.yellow, children: B.v }), _jsx(Text, { children: centerPad(SHORTCUTS.map(s => `[${s.key}] ${s.desc}`).join(' '), innerWidth) }), _jsx(Text, { color: C.yellow, children: B.v })] }), _jsx(EmptyRow, { width: innerWidth }), _jsx(Text, { children: _jsxs(Text, { color: C.yellow, children: [B.bottomLeft, B.h.repeat(innerWidth), B.bottomRight] }) })] }));
114
+ return (_jsx(HorizontalHeader, { modelName: modelName, isConnected: isConnected, sessionCount: sessions.length, modeLabel: modeLabel, theme: theme, tip: tip, sessions: sessions, version: VERSION }));
207
115
  };
208
116
  //# sourceMappingURL=Header.js.map
@@ -1,22 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
- import { Static, Box } from 'ink';
2
+ import { Box } from 'ink';
4
3
  import { useChatState, StreamingState } from '../contexts/ChatContext.js';
5
4
  import { HistoryItemDisplay } from './HistoryItemDisplay.js';
6
5
  import { AssistantMessage } from './messages/AssistantMessage.js';
7
6
  import { Header } from './Header.js';
8
- // Stable sentinel object — always the first item so <Static> renders
9
- // the Header once, and subsequent history items are appended after it.
10
- const HEADER_SENTINEL = { __header: true, id: '__header__' };
11
7
  export const MainContent = () => {
12
- const { history, pendingContent, streamingState, sessionVersion } = useChatState();
13
- // Always keep the sentinel at index 0 so Static.length grows correctly.
14
- const staticItems = useMemo(() => [HEADER_SENTINEL, ...history], [history]);
15
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Static, { items: staticItems, children: (item) => {
16
- if ('__header' in item) {
17
- return (_jsx(Box, { children: _jsx(Header, {}) }, "__header__"));
18
- }
19
- return (_jsx(Box, { paddingX: 1, children: _jsx(HistoryItemDisplay, { item: item }) }, item.id));
20
- } }, sessionVersion), streamingState !== StreamingState.Idle && (_jsx(Box, { paddingX: 1, children: _jsx(AssistantMessage, { text: pendingContent, isPending: true }) }))] }));
8
+ const { history, pendingContent, streamingState } = useChatState();
9
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, {}), history.map((item) => (_jsx(Box, { paddingX: 1, children: _jsx(HistoryItemDisplay, { item: item }) }, item.id))), streamingState !== StreamingState.Idle && (_jsx(Box, { paddingX: 1, children: _jsx(AssistantMessage, { text: pendingContent, isPending: true }) }))] }));
21
10
  };
22
11
  //# sourceMappingURL=MainContent.js.map
@@ -1,28 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useMemo } from 'react';
3
3
  import { Text } from 'ink';
4
- import { Marked } from 'marked';
5
- import { markedTerminal } from 'marked-terminal';
6
- import { supportsLanguage } from 'cli-highlight';
7
- const marked = new Marked(markedTerminal({
8
- reflowText: true,
9
- width: 80,
10
- showSectionPrefix: false,
11
- tab: 2,
12
- }));
13
- // Replace ```<unsupported-lang> with ``` to avoid console.warn from highlight.js
14
- function silentParse(src) {
15
- // Strip internal heading markers (e.g. {mist_session_heading:...})
16
- let cleaned = src.replace(/\{mist_session_heading:[^}]*\}\s*/g, '');
17
- cleaned = cleaned.replace(/^```(\w+)/gm, (_match, lang) => supportsLanguage(lang) ? '```' + lang : '```');
18
- return marked.parse(cleaned);
19
- }
4
+ import { applyMarkdown } from '../../utils/markdown.js';
20
5
  export const MarkdownRenderer = React.memo(({ text, }) => {
21
6
  const rendered = useMemo(() => {
22
7
  if (!text)
23
8
  return null;
24
9
  try {
25
- return silentParse(text).trimEnd();
10
+ return applyMarkdown(text);
26
11
  }
27
12
  catch {
28
13
  return text;
@@ -1,9 +1,28 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
2
+ import { useRef } from 'react';
3
+ import { Box, Text, useCursor } from 'ink';
3
4
  import chalk from 'chalk';
5
+ import stringWidth from 'string-width';
4
6
  import { cpSlice, cpLen } from '../../utils/textUtils.js';
5
7
  import { PASTE_PLACEHOLDER_REGEX } from '../../utils/pasteUtils.js';
6
8
  import { palette } from '../../utils/colors.js';
9
+ /**
10
+ * Walk up the Yoga layout tree to compute absolute (x, y) of a DOM node.
11
+ * Same algorithm as Ink's internal `getAbsolutePosition` (layout.ts).
12
+ */
13
+ function getAbsolutePosition(node) {
14
+ let x = 0;
15
+ let y = 0;
16
+ let cur = node;
17
+ while (cur?.parentNode) {
18
+ if (!cur.yogaNode)
19
+ return null;
20
+ x += cur.yogaNode.getComputedLeft();
21
+ y += cur.yogaNode.getComputedTop();
22
+ cur = cur.parentNode;
23
+ }
24
+ return { x, y };
25
+ }
7
26
  /**
8
27
  * Split a line into segments, marking paste placeholders for accent rendering.
9
28
  * Returns array of { text, isPaste } segments.
@@ -93,10 +112,50 @@ export const TextInput = ({ buffer, placeholder = 'Ask anything...', isActive, }
93
112
  const { viewportVisualLines, visualCursor } = buffer;
94
113
  const [cursorRow, cursorCol] = visualCursor;
95
114
  const showPlaceholder = buffer.text.length === 0;
115
+ // ── Terminal cursor positioning for IME ──
116
+ // Park the real terminal cursor at the text caret so CJK IME candidate
117
+ // windows appear inline instead of at the end of the line.
118
+ const { setCursorPosition } = useCursor();
119
+ const boxRef = useRef(null);
120
+ if (!isActive) {
121
+ setCursorPosition(undefined);
122
+ }
123
+ else {
124
+ const node = boxRef.current;
125
+ if (node) {
126
+ const abs = getAbsolutePosition(node);
127
+ if (abs) {
128
+ const indent = cursorRow === 0 ? 0 : 2;
129
+ const lineText = viewportVisualLines[cursorRow] ?? '';
130
+ const textBeforeCursor = cpSlice(lineText, 0, cursorCol);
131
+ const displayCol = stringWidth(textBeforeCursor);
132
+ // Ink's buildCursorSuffix computes: moveUp = visibleLineCount - y.
133
+ // When output fits in terminal, the output string ends with '\n'
134
+ // and visibleLineCount = lines.length - 1 (= Yoga rootH). The
135
+ // cursor sits one line below the last visible line, so Yoga's
136
+ // absolute Y maps directly.
137
+ //
138
+ // When output overflows (rootH > termRows), Ink's incremental
139
+ // renderer omits the trailing '\n', so visibleLineCount = lines.length
140
+ // (= rootH + 1 effectively in the moveUp math). The cursor sits at
141
+ // the END of the last line, not a line below it. This shifts the
142
+ // moveUp origin by one row, requiring y + 1 to compensate.
143
+ let rootNode = node;
144
+ while (rootNode?.parentNode)
145
+ rootNode = rootNode.parentNode;
146
+ const rootH = rootNode?.yogaNode?.getComputedHeight() ?? 0;
147
+ const overflows = rootH > (process.stdout.rows ?? 24);
148
+ setCursorPosition({
149
+ x: abs.x + indent + displayCol,
150
+ y: abs.y + cursorRow + (overflows ? 1 : 0),
151
+ });
152
+ }
153
+ }
154
+ }
96
155
  if (showPlaceholder) {
97
- return (_jsx(Box, { children: isActive ? (_jsxs(Text, { children: [chalk.inverse(placeholder[0] || ' '), _jsx(Text, { color: palette.textDim, children: placeholder.slice(1) })] })) : (_jsx(Text, { color: palette.textDim, children: placeholder })) }));
156
+ return (_jsx(Box, { ref: boxRef, children: isActive ? (_jsxs(Text, { children: [chalk.inverse(placeholder[0] || ' '), _jsx(Text, { color: palette.textDim, children: placeholder.slice(1) })] })) : (_jsx(Text, { color: palette.textDim, children: placeholder })) }));
98
157
  }
99
- return (_jsx(Box, { flexDirection: "column", children: viewportVisualLines.map((lineText, idx) => {
158
+ return (_jsx(Box, { ref: boxRef, flexDirection: "column", children: viewportVisualLines.map((lineText, idx) => {
100
159
  const isCursorLine = isActive && idx === cursorRow;
101
160
  // First line has no indent (follows ❯ prompt), subsequent lines indent 2 spaces
102
161
  const indent = idx === 0 ? '' : ' ';
@@ -3,6 +3,7 @@ import { createParser } from 'eventsource-parser';
3
3
  import { writeFileSync, mkdirSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { chatApi } from '../api/chat.js';
6
+ import { ApiError } from '../api/client.js';
6
7
  import { submitTunnelResult } from '../api/tunnel.js';
7
8
  import { processFileReferences } from '../utils/fileRef.js';
8
9
  import { executeFileOperation, isSensitiveOperation, isAlwaysConfirmRequired } from '../utils/fileTunnel.js';
@@ -292,7 +293,17 @@ export function useChat() {
292
293
  }
293
294
  catch (err) {
294
295
  tokenBuf.dispose();
295
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
296
+ let errorMessage = err instanceof Error ? err.message : 'Unknown error';
297
+ if (err instanceof ApiError) {
298
+ try {
299
+ const parsed = JSON.parse(err.body);
300
+ if (parsed.detail?.message)
301
+ errorMessage = parsed.detail.message;
302
+ else if (typeof parsed.detail === 'string')
303
+ errorMessage = parsed.detail;
304
+ }
305
+ catch { /* use default */ }
306
+ }
296
307
  dispatch({ type: 'STREAM_ERROR', error: errorMessage });
297
308
  }
298
309
  finally {
@@ -421,7 +432,17 @@ export function useChat() {
421
432
  await processConfirmStream(reader);
422
433
  }
423
434
  catch (err) {
424
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
435
+ let errorMessage = err instanceof Error ? err.message : 'Unknown error';
436
+ if (err instanceof ApiError) {
437
+ try {
438
+ const parsed = JSON.parse(err.body);
439
+ if (parsed.detail?.message)
440
+ errorMessage = parsed.detail.message;
441
+ else if (typeof parsed.detail === 'string')
442
+ errorMessage = parsed.detail;
443
+ }
444
+ catch { /* use default */ }
445
+ }
425
446
  dispatch({ type: 'STREAM_ERROR', error: errorMessage });
426
447
  }
427
448
  }, [state.threadId, state.pendingPlan, dispatch, processConfirmStream]);
@@ -1,5 +1,6 @@
1
1
  import { useCallback } from 'react';
2
2
  import { sessionsApi } from '../api/sessions.js';
3
+ import { quotaApi } from '../api/quota.js';
3
4
  import { useChatState, useChatDispatch } from '../contexts/ChatContext.js';
4
5
  import { useAppState, useAppActions } from '../contexts/AppContext.js';
5
6
  import { useUIActions } from '../contexts/UIContext.js';
@@ -104,6 +105,50 @@ export function useSlashCommand() {
104
105
  });
105
106
  return;
106
107
  }
108
+ // /usage — show account balance and usage
109
+ if (trimmed === '/usage') {
110
+ dispatch({ type: 'SET_BUSY', busy: true });
111
+ try {
112
+ const b = await quotaApi.balance();
113
+ const fmt = (v) => `$${v.toFixed(4)}`;
114
+ const fmtTokens = (v) => v >= 1_000_000
115
+ ? `${(v / 1_000_000).toFixed(1)}M`
116
+ : v >= 1_000
117
+ ? `${(v / 1_000).toFixed(1)}K`
118
+ : String(v);
119
+ const lines = [
120
+ `**账户余额**`,
121
+ ` 余额: ${fmt(b.usd_balance)}`,
122
+ ` 总充值: ${fmt(b.usd_total_granted)}`,
123
+ ` 总消耗: ${fmt(b.usd_total_consumed)}`,
124
+ ``,
125
+ `**今日用量**`,
126
+ ` Tokens: ${fmtTokens(b.usage_today_tokens)}`,
127
+ ` 费用: ${fmt(b.cost_today_usd)}`,
128
+ ``,
129
+ `**本月用量**`,
130
+ ` Tokens: ${fmtTokens(b.usage_this_month_tokens)}`,
131
+ ` 费用: ${fmt(b.cost_this_month_usd)}`,
132
+ ];
133
+ dispatch({
134
+ type: 'ADD_ITEM',
135
+ item: { type: 'command', command: '/usage', result: lines.join('\n') },
136
+ });
137
+ }
138
+ catch (err) {
139
+ dispatch({
140
+ type: 'ADD_ITEM',
141
+ item: {
142
+ type: 'error',
143
+ text: `查询用量失败: ${err instanceof Error ? err.message : String(err)}`,
144
+ },
145
+ });
146
+ }
147
+ finally {
148
+ dispatch({ type: 'SET_BUSY', busy: false });
149
+ }
150
+ return;
151
+ }
107
152
  // /quit or /exit
108
153
  if (trimmed === '/quit' || trimmed === '/exit') {
109
154
  dispatch({