@serjm/deepseek-code 0.3.1
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.
- package/CONTRIBUTING.md +73 -0
- package/README.md +194 -0
- package/README.ru.md +194 -0
- package/dist/api/index.d.ts +77 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +263 -0
- package/dist/api/index.js.map +1 -0
- package/dist/cli/headless.d.ts +22 -0
- package/dist/cli/headless.d.ts.map +1 -0
- package/dist/cli/headless.js +122 -0
- package/dist/cli/headless.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +90 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/interactive.d.ts +18 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +75 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/commands/index.d.ts +30 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +964 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/config/defaults.d.ts +37 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +39 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +76 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/core/agent-loop.d.ts +111 -0
- package/dist/core/agent-loop.d.ts.map +1 -0
- package/dist/core/agent-loop.js +485 -0
- package/dist/core/agent-loop.js.map +1 -0
- package/dist/core/checkpoint.d.ts +10 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +83 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/extensions.d.ts +55 -0
- package/dist/core/extensions.d.ts.map +1 -0
- package/dist/core/extensions.js +113 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/git.d.ts +68 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +148 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/hooks.d.ts +37 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +77 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/i18n.d.ts +90 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +253 -0
- package/dist/core/i18n.js.map +1 -0
- package/dist/core/lsp.d.ts +74 -0
- package/dist/core/lsp.d.ts.map +1 -0
- package/dist/core/lsp.js +239 -0
- package/dist/core/lsp.js.map +1 -0
- package/dist/core/mcp.d.ts +49 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +195 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/memory.d.ts +38 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +231 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/metrics.d.ts +36 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +111 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/review.d.ts +27 -0
- package/dist/core/review.d.ts.map +1 -0
- package/dist/core/review.js +201 -0
- package/dist/core/review.js.map +1 -0
- package/dist/core/sandbox.d.ts +52 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandbox.js +140 -0
- package/dist/core/sandbox.js.map +1 -0
- package/dist/core/scheduler.d.ts +56 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +167 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/session.d.ts +49 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +127 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/skills.d.ts +36 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +90 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/subagent.d.ts +45 -0
- package/dist/core/subagent.d.ts.map +1 -0
- package/dist/core/subagent.js +130 -0
- package/dist/core/subagent.js.map +1 -0
- package/dist/core/themes.d.ts +35 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/themes.js +188 -0
- package/dist/core/themes.js.map +1 -0
- package/dist/tools/bash.d.ts +3 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +92 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/chrome-manager.d.ts +35 -0
- package/dist/tools/chrome-manager.d.ts.map +1 -0
- package/dist/tools/chrome-manager.js +163 -0
- package/dist/tools/chrome-manager.js.map +1 -0
- package/dist/tools/chrome.d.ts +78 -0
- package/dist/tools/chrome.d.ts.map +1 -0
- package/dist/tools/chrome.js +1058 -0
- package/dist/tools/chrome.js.map +1 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +81 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +3 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +41 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +74 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/path-safety.d.ts +3 -0
- package/dist/tools/path-safety.d.ts.map +1 -0
- package/dist/tools/path-safety.js +19 -0
- package/dist/tools/path-safety.js.map +1 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +58 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +43 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +47 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +90 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +51 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/ui/activity-cards.d.ts +50 -0
- package/dist/ui/activity-cards.d.ts.map +1 -0
- package/dist/ui/activity-cards.js +185 -0
- package/dist/ui/activity-cards.js.map +1 -0
- package/dist/ui/app.d.ts +9 -0
- package/dist/ui/app.d.ts.map +1 -0
- package/dist/ui/app.js +852 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/chat-view.d.ts +10 -0
- package/dist/ui/chat-view.d.ts.map +1 -0
- package/dist/ui/chat-view.js +94 -0
- package/dist/ui/chat-view.js.map +1 -0
- package/dist/ui/error-boundary.d.ts +13 -0
- package/dist/ui/error-boundary.d.ts.map +1 -0
- package/dist/ui/error-boundary.js +16 -0
- package/dist/ui/error-boundary.js.map +1 -0
- package/dist/ui/fade-in.d.ts +8 -0
- package/dist/ui/fade-in.d.ts.map +1 -0
- package/dist/ui/fade-in.js +14 -0
- package/dist/ui/fade-in.js.map +1 -0
- package/dist/ui/input-bar.d.ts +16 -0
- package/dist/ui/input-bar.d.ts.map +1 -0
- package/dist/ui/input-bar.js +269 -0
- package/dist/ui/input-bar.js.map +1 -0
- package/dist/ui/markdown-view.d.ts +9 -0
- package/dist/ui/markdown-view.d.ts.map +1 -0
- package/dist/ui/markdown-view.js +240 -0
- package/dist/ui/markdown-view.js.map +1 -0
- package/dist/ui/matrix-rain.d.ts +2 -0
- package/dist/ui/matrix-rain.d.ts.map +1 -0
- package/dist/ui/matrix-rain.js +134 -0
- package/dist/ui/matrix-rain.js.map +1 -0
- package/dist/ui/reasoning-view.d.ts +12 -0
- package/dist/ui/reasoning-view.d.ts.map +1 -0
- package/dist/ui/reasoning-view.js +34 -0
- package/dist/ui/reasoning-view.js.map +1 -0
- package/dist/ui/results-panel.d.ts +11 -0
- package/dist/ui/results-panel.d.ts.map +1 -0
- package/dist/ui/results-panel.js +17 -0
- package/dist/ui/results-panel.js.map +1 -0
- package/dist/ui/setup-wizard.d.ts +30 -0
- package/dist/ui/setup-wizard.d.ts.map +1 -0
- package/dist/ui/setup-wizard.js +166 -0
- package/dist/ui/setup-wizard.js.map +1 -0
- package/dist/ui/status-bar.d.ts +14 -0
- package/dist/ui/status-bar.d.ts.map +1 -0
- package/dist/ui/status-bar.js +63 -0
- package/dist/ui/status-bar.js.map +1 -0
- package/dist/ui/tool-activity-card.d.ts +9 -0
- package/dist/ui/tool-activity-card.d.ts.map +1 -0
- package/dist/ui/tool-activity-card.js +172 -0
- package/dist/ui/tool-activity-card.js.map +1 -0
- package/dist/ui/tool-call-view.d.ts +9 -0
- package/dist/ui/tool-call-view.d.ts.map +1 -0
- package/dist/ui/tool-call-view.js +149 -0
- package/dist/ui/tool-call-view.js.map +1 -0
- package/dist/utils/clipboard.d.ts +6 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js +56 -0
- package/dist/utils/clipboard.js.map +1 -0
- package/dist/utils/ignore.d.ts +6 -0
- package/dist/utils/ignore.d.ts.map +1 -0
- package/dist/utils/ignore.js +40 -0
- package/dist/utils/ignore.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +13 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/string-width.d.ts +6 -0
- package/dist/utils/string-width.d.ts.map +1 -0
- package/dist/utils/string-width.js +37 -0
- package/dist/utils/string-width.js.map +1 -0
- package/package.json +68 -0
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useApp, useStdin } from 'ink';
|
|
4
|
+
import { ChatView } from './chat-view.js';
|
|
5
|
+
import { InputBar } from './input-bar.js';
|
|
6
|
+
import { StatusBar } from './status-bar.js';
|
|
7
|
+
import { DEEPSEEK_MODELS } from '../config/defaults.js';
|
|
8
|
+
import { saveConfig } from '../config/loader.js';
|
|
9
|
+
import { AgentLoop } from '../core/agent-loop.js';
|
|
10
|
+
import { saveSession, getLastSessionId, writeExecutionBundle, writeSessionHandoff } from '../core/session.js';
|
|
11
|
+
import { hooksManager } from '../core/hooks.js';
|
|
12
|
+
import { mcpManager } from '../core/mcp.js';
|
|
13
|
+
import { subAgentManager } from '../core/subagent.js';
|
|
14
|
+
import { skillsManager } from '../core/skills.js';
|
|
15
|
+
import { lspManager } from '../core/lsp.js';
|
|
16
|
+
import { scheduler } from '../core/scheduler.js';
|
|
17
|
+
import { themeManager } from '../core/themes.js';
|
|
18
|
+
import { i18n } from '../core/i18n.js';
|
|
19
|
+
import { Logo, SetupWizard, useSetupWizard } from './setup-wizard.js';
|
|
20
|
+
import { executeSlashCommand } from '../commands/index.js';
|
|
21
|
+
/** Empty input hint timeout in ms before showing the guide text */
|
|
22
|
+
const EMPTY_INPUT_HINT_DELAY = 2000;
|
|
23
|
+
function stripExecutionSummary(content) {
|
|
24
|
+
return content.replace(/\n\n━━━ Execution Summary ━━━[\s\S]*$/u, '').trimEnd();
|
|
25
|
+
}
|
|
26
|
+
function historyForModel(messages) {
|
|
27
|
+
return messages.flatMap(message => {
|
|
28
|
+
if (message.role === 'tool') {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = typeof message.content === 'string' ? JSON.parse(message.content) : message.content;
|
|
31
|
+
if (parsed?.type === 'tool_activity_card')
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Keep non-UI tool messages.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (message.role === 'assistant' && typeof message.content === 'string') {
|
|
39
|
+
return [{ ...message, content: stripExecutionSummary(message.content) }];
|
|
40
|
+
}
|
|
41
|
+
return [message];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Setup wizard step components are now in ./setup-wizard.tsx
|
|
45
|
+
// Logo, SetupWizard, useSetupWizard imported from there
|
|
46
|
+
export function App({ config, options }) {
|
|
47
|
+
const { exit } = useApp();
|
|
48
|
+
const [approvalMode, setApprovalMode] = useState(options.approvalMode ?? (options.turbo ? 'turbo' : config.approvalMode));
|
|
49
|
+
const [messages, setMessages] = useState([]);
|
|
50
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
51
|
+
const [statusText, setStatusText] = useState(i18n.t('ready'));
|
|
52
|
+
const [localApiKey, setLocalApiKey] = useState(config.apiKey || '');
|
|
53
|
+
const agentLoopRef = useRef(null);
|
|
54
|
+
const abortControllerRef = useRef(null);
|
|
55
|
+
const pendingApprovalResolveRef = useRef(null);
|
|
56
|
+
const liveToolMessageIndexRef = useRef(-1);
|
|
57
|
+
const prevToolCallsRef = useRef([]);
|
|
58
|
+
const [toolCalls, setToolCalls] = useState([]);
|
|
59
|
+
const [pendingApproval, setPendingApproval] = useState(null);
|
|
60
|
+
const [approvalCursor, setApprovalCursor] = useState(0);
|
|
61
|
+
const [pendingClear, setPendingClear] = useState(false);
|
|
62
|
+
const [clearCursor, setClearCursor] = useState(0);
|
|
63
|
+
const exemptedToolsRef = useRef(new Set());
|
|
64
|
+
const approvalModeRef = useRef(approvalMode);
|
|
65
|
+
const sessionIdRef = useRef('');
|
|
66
|
+
const initializedRef = useRef(false);
|
|
67
|
+
const [emptyInputHint, setEmptyInputHint] = useState(false);
|
|
68
|
+
const emptyInputTimerRef = useRef(null);
|
|
69
|
+
const [chatScrollOffset, setChatScrollOffset] = useState(0);
|
|
70
|
+
const [scrollMode, setScrollMode] = useState('follow');
|
|
71
|
+
const [newMessagesWhilePaused, setNewMessagesWhilePaused] = useState(false);
|
|
72
|
+
const visibleMessageCountRef = useRef(0);
|
|
73
|
+
const [contextPercent, setContextPercent] = useState(0);
|
|
74
|
+
const [totalTokens, setTotalTokens] = useState(0);
|
|
75
|
+
const [estimatedCost, setEstimatedCost] = useState(0);
|
|
76
|
+
const [pendingImage, setPendingImage] = useState(null);
|
|
77
|
+
const [themePicker, setThemePicker] = useState(null);
|
|
78
|
+
const [modelPicker, setModelPicker] = useState(null);
|
|
79
|
+
const [serviceNotice, setServiceNotice] = useState(null);
|
|
80
|
+
const serviceNoticeTimerRef = useRef(null);
|
|
81
|
+
const addServiceNotice = useCallback((text) => {
|
|
82
|
+
setServiceNotice(text);
|
|
83
|
+
if (serviceNoticeTimerRef.current)
|
|
84
|
+
clearTimeout(serviceNoticeTimerRef.current);
|
|
85
|
+
serviceNoticeTimerRef.current = setTimeout(() => setServiceNotice(null), 3000);
|
|
86
|
+
}, []);
|
|
87
|
+
// Keep approvalModeRef in sync so onApprovalRequest always uses the current mode
|
|
88
|
+
// even when changed via Tab while the agent is running
|
|
89
|
+
useEffect(() => { approvalModeRef.current = approvalMode; }, [approvalMode]);
|
|
90
|
+
// Check if API key is configured (non-empty) — used early for locale detection
|
|
91
|
+
const hasApiKey = !!(config.apiKey || process.env.DEEPSEEK_API_KEY) &&
|
|
92
|
+
(config.apiKey || process.env.DEEPSEEK_API_KEY || '').trim().length > 0;
|
|
93
|
+
// Set locale from config on mount (before first render)
|
|
94
|
+
// Auto-detect system locale on first launch (when no API key is configured)
|
|
95
|
+
const localeRef = useRef(false);
|
|
96
|
+
if (!localeRef.current) {
|
|
97
|
+
localeRef.current = true;
|
|
98
|
+
const lang = (config.language || (hasApiKey ? 'en' : i18n.detectLocale()));
|
|
99
|
+
i18n.setLocale(lang);
|
|
100
|
+
}
|
|
101
|
+
// Setup wizard — use the hook from setup-wizard.tsx
|
|
102
|
+
const initialStep = hasApiKey ? 'done' : 'lang';
|
|
103
|
+
const [setupWizardState, setupWizardActions] = useSetupWizard(config, initialStep, (updatedConfig) => {
|
|
104
|
+
setLocalApiKey(updatedConfig.apiKey || '');
|
|
105
|
+
setApprovalMode(updatedConfig.approvalMode || 'default');
|
|
106
|
+
}, (msg) => {
|
|
107
|
+
setMessages(prev => [...prev, msg]);
|
|
108
|
+
});
|
|
109
|
+
const { step: setupStep, apiKeyError, langCursor, themeCursor, modelCursor, modeCursor, langOptions, modeOptions } = setupWizardState;
|
|
110
|
+
const { handleApiKeySubmit, finishSetup, setLangCursor, setThemeCursor, setModelCursor, setModeCursor, setStep: setSetupStep, setApiKeyError } = setupWizardActions;
|
|
111
|
+
const setupStepRef = useRef(setupStep);
|
|
112
|
+
setupStepRef.current = setupStep;
|
|
113
|
+
// Initialize session and services on mount
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (initializedRef.current)
|
|
116
|
+
return;
|
|
117
|
+
initializedRef.current = true;
|
|
118
|
+
// Set locale from config on startup
|
|
119
|
+
// (already set above, but keep for safety)
|
|
120
|
+
(async () => {
|
|
121
|
+
if (options.continue_) {
|
|
122
|
+
const lastId = await getLastSessionId();
|
|
123
|
+
if (lastId)
|
|
124
|
+
sessionIdRef.current = lastId;
|
|
125
|
+
}
|
|
126
|
+
if (!sessionIdRef.current) {
|
|
127
|
+
sessionIdRef.current = await saveSession({});
|
|
128
|
+
}
|
|
129
|
+
subAgentManager.setApiConfig({ ...config, apiKey: localApiKey || config.apiKey });
|
|
130
|
+
// Initialize services in background
|
|
131
|
+
await Promise.allSettled([
|
|
132
|
+
mcpManager.loadConfig().then(() => mcpManager.connectAll()),
|
|
133
|
+
skillsManager.loadAll(),
|
|
134
|
+
hooksManager.load(),
|
|
135
|
+
lspManager.load().then(() => lspManager.initializeAll()),
|
|
136
|
+
subAgentManager.loadFromDir(),
|
|
137
|
+
scheduler.load(),
|
|
138
|
+
]);
|
|
139
|
+
// Chrome не инициализируется при старте.
|
|
140
|
+
// Он запускается только когда:
|
|
141
|
+
// - агент вызывает chrome tool
|
|
142
|
+
// - пользователь запускает /browser-test
|
|
143
|
+
// - пользователь запускает /chrome
|
|
144
|
+
setStatusText(i18n.t('ready'));
|
|
145
|
+
})();
|
|
146
|
+
}, []);
|
|
147
|
+
// Register soft-cancel hook for the SIGINT handler in interactive.ts.
|
|
148
|
+
// While isProcessing, interactive.ts's onSIGINT calls this instead of process.exit().
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const proc = process;
|
|
151
|
+
if (isProcessing) {
|
|
152
|
+
proc.__agentSoftCancel = () => {
|
|
153
|
+
abortControllerRef.current?.abort();
|
|
154
|
+
if (pendingApprovalResolveRef.current) {
|
|
155
|
+
pendingApprovalResolveRef.current(false);
|
|
156
|
+
pendingApprovalResolveRef.current = null;
|
|
157
|
+
}
|
|
158
|
+
setPendingApproval(null);
|
|
159
|
+
setStatusText(i18n.t('cancelled'));
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
proc.__agentSoftCancel = undefined;
|
|
164
|
+
}
|
|
165
|
+
return () => { proc.__agentSoftCancel = undefined; };
|
|
166
|
+
}, [isProcessing]);
|
|
167
|
+
const { stdin } = useStdin();
|
|
168
|
+
// Compensate scroll offset when new visible messages arrive while paused
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (setupStepRef.current !== 'done')
|
|
171
|
+
return;
|
|
172
|
+
const visible = messages.filter(m => m.role !== 'tool').length;
|
|
173
|
+
if (scrollMode === 'paused' && visible > visibleMessageCountRef.current) {
|
|
174
|
+
const diff = visible - visibleMessageCountRef.current;
|
|
175
|
+
setChatScrollOffset(prev => prev + diff);
|
|
176
|
+
setNewMessagesWhilePaused(true);
|
|
177
|
+
}
|
|
178
|
+
visibleMessageCountRef.current = visible;
|
|
179
|
+
}, [messages.length, scrollMode]);
|
|
180
|
+
// Sync prevToolCallsRef with toolCalls state
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
prevToolCallsRef.current = toolCalls;
|
|
183
|
+
}, [toolCalls]);
|
|
184
|
+
// Detect End key from raw stdin (Ink does not expose it in useInput)
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!stdin)
|
|
187
|
+
return;
|
|
188
|
+
const handler = (data) => {
|
|
189
|
+
if (setupStepRef.current !== 'done')
|
|
190
|
+
return;
|
|
191
|
+
const seq = data.toString();
|
|
192
|
+
if (seq === '\x1b[F' || seq === '\x1b[4~' || seq === '\x1b[8~' || seq === '\x1bOF') {
|
|
193
|
+
setChatScrollOffset(0);
|
|
194
|
+
setScrollMode('follow');
|
|
195
|
+
setNewMessagesWhilePaused(false);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
stdin.on('data', handler);
|
|
199
|
+
return () => { stdin.off('data', handler); };
|
|
200
|
+
}, [stdin]);
|
|
201
|
+
// Slash commands — delegated to commands/index.ts
|
|
202
|
+
const handleSlashCommand = useCallback(async (input) => {
|
|
203
|
+
const ctx = {
|
|
204
|
+
config,
|
|
205
|
+
approvalMode,
|
|
206
|
+
messages,
|
|
207
|
+
setMessages,
|
|
208
|
+
setStatusText,
|
|
209
|
+
setSetupStep,
|
|
210
|
+
addServiceNotice,
|
|
211
|
+
getMetrics: () => agentLoopRef.current?.getMetrics(),
|
|
212
|
+
onThemePicker: () => {
|
|
213
|
+
const themes = themeManager.listThemes();
|
|
214
|
+
const currentName = themeManager.theme.name;
|
|
215
|
+
const idx = themes.findIndex(t => t.name === currentName);
|
|
216
|
+
setThemePicker({ themes, selectedIndex: Math.max(0, idx) });
|
|
217
|
+
},
|
|
218
|
+
onModelPicker: () => {
|
|
219
|
+
const idx = DEEPSEEK_MODELS.findIndex(m => m.id === config.model);
|
|
220
|
+
setModelPicker({ selectedIndex: Math.max(0, idx) });
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
return executeSlashCommand(input, ctx);
|
|
224
|
+
}, [config, approvalMode, messages, addServiceNotice]);
|
|
225
|
+
const handleSubmit = useCallback(async (input) => {
|
|
226
|
+
// Show hint on empty input
|
|
227
|
+
if (!input.trim()) {
|
|
228
|
+
setEmptyInputHint(true);
|
|
229
|
+
if (emptyInputTimerRef.current)
|
|
230
|
+
clearTimeout(emptyInputTimerRef.current);
|
|
231
|
+
emptyInputTimerRef.current = setTimeout(() => setEmptyInputHint(false), EMPTY_INPUT_HINT_DELAY);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (isProcessing)
|
|
235
|
+
return;
|
|
236
|
+
// Handle setup wizard steps
|
|
237
|
+
if (setupStep === 'apikey') {
|
|
238
|
+
await handleApiKeySubmit(input);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (setupStep === 'theme' || setupStep === 'mode') {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (setupStep !== 'done') {
|
|
245
|
+
setMessages(prev => [...prev, {
|
|
246
|
+
role: 'assistant',
|
|
247
|
+
content: 'Сначала завершите настройку. Используйте /setup для повторного запуска.',
|
|
248
|
+
}]);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (input.startsWith('/')) {
|
|
252
|
+
try {
|
|
253
|
+
const handled = await handleSlashCommand(input);
|
|
254
|
+
if (handled)
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
setMessages(prev => [...prev, {
|
|
259
|
+
role: 'assistant',
|
|
260
|
+
content: `${i18n.t('error')}: ${err.message}`,
|
|
261
|
+
}]);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Unknown slash command — show local error, do NOT send to model
|
|
265
|
+
setMessages(prev => [...prev, {
|
|
266
|
+
role: 'assistant',
|
|
267
|
+
content: `Неизвестная команда: \`${input.trim().split(/\s+/)[0]}\`. Введите \`/help\` для списка команд.`,
|
|
268
|
+
}]);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Abort previous request if still running
|
|
272
|
+
if (abortControllerRef.current) {
|
|
273
|
+
abortControllerRef.current.abort();
|
|
274
|
+
}
|
|
275
|
+
const abortController = new AbortController();
|
|
276
|
+
abortControllerRef.current = abortController;
|
|
277
|
+
let userContent = input;
|
|
278
|
+
if (pendingImage) {
|
|
279
|
+
userContent = [
|
|
280
|
+
{ type: 'text', text: input },
|
|
281
|
+
{ type: 'image_url', image_url: { url: `data:${pendingImage.mimeType};base64,${pendingImage.base64}` } },
|
|
282
|
+
];
|
|
283
|
+
setPendingImage(null);
|
|
284
|
+
}
|
|
285
|
+
const userMessage = { role: 'user', content: userContent };
|
|
286
|
+
setMessages(prev => [...prev, userMessage]);
|
|
287
|
+
setIsProcessing(true);
|
|
288
|
+
setStatusText(i18n.t('working'));
|
|
289
|
+
setToolCalls([]);
|
|
290
|
+
liveToolMessageIndexRef.current = -1;
|
|
291
|
+
setChatScrollOffset(0);
|
|
292
|
+
setScrollMode('follow');
|
|
293
|
+
setNewMessagesWhilePaused(false);
|
|
294
|
+
try {
|
|
295
|
+
await hooksManager.execute('UserPromptSubmit', {
|
|
296
|
+
event: 'UserPromptSubmit',
|
|
297
|
+
projectDir: process.cwd(),
|
|
298
|
+
});
|
|
299
|
+
// Create fresh AgentLoop every time (avoids stale closures)
|
|
300
|
+
agentLoopRef.current = new AgentLoop({ ...config, apiKey: localApiKey || config.apiKey }, {
|
|
301
|
+
approvalMode,
|
|
302
|
+
cwd: process.cwd(),
|
|
303
|
+
signal: abortController.signal,
|
|
304
|
+
onToolCall: (tc) => {
|
|
305
|
+
const updatedCalls = [...prevToolCallsRef.current, tc];
|
|
306
|
+
prevToolCallsRef.current = updatedCalls;
|
|
307
|
+
setToolCalls(updatedCalls);
|
|
308
|
+
setStatusText(`🔧 ${tc.name}...`);
|
|
309
|
+
// Add/update live tool activity card in chat messages
|
|
310
|
+
setMessages(prev => {
|
|
311
|
+
const idx = liveToolMessageIndexRef.current;
|
|
312
|
+
const card = { type: 'tool_activity_card', toolCalls: updatedCalls, status: 'live' };
|
|
313
|
+
if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
|
|
314
|
+
// Update existing card
|
|
315
|
+
const updated = [...prev];
|
|
316
|
+
updated[idx] = { role: 'tool', content: JSON.stringify(card) };
|
|
317
|
+
return updated;
|
|
318
|
+
}
|
|
319
|
+
// Add new card
|
|
320
|
+
liveToolMessageIndexRef.current = prev.length;
|
|
321
|
+
return [...prev, { role: 'tool', content: JSON.stringify(card) }];
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
onToolResult: (result) => {
|
|
325
|
+
setStatusText(result.success ? `✅ ${result.toolName} ${i18n.t('toolDone')}` : `❌ ${result.toolName} ${i18n.t('toolError')}`);
|
|
326
|
+
},
|
|
327
|
+
onReasoningChunk: () => { },
|
|
328
|
+
onStreamChunk: (chunk) => {
|
|
329
|
+
setMessages(prev => {
|
|
330
|
+
const last = prev[prev.length - 1];
|
|
331
|
+
if (last?.role === 'assistant') {
|
|
332
|
+
// If last content is empty, replace with chunk; otherwise append
|
|
333
|
+
const updated = [...prev];
|
|
334
|
+
updated[updated.length - 1] = { ...last, content: last.content + chunk };
|
|
335
|
+
return updated;
|
|
336
|
+
}
|
|
337
|
+
return [...prev, { role: 'assistant', content: chunk }];
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
// onResponse intentionally removed — onStreamChunk handles all text
|
|
341
|
+
// Avoids duplicate "assistant" message push that caused text doubling
|
|
342
|
+
onResponse: () => { },
|
|
343
|
+
onError: () => {
|
|
344
|
+
// Handled by handleSubmit catch block — adding here would create duplicate
|
|
345
|
+
// assistant messages, breaking the conversation structure for the next request
|
|
346
|
+
},
|
|
347
|
+
onApprovalRequest: async (toolName, args) => {
|
|
348
|
+
if (approvalModeRef.current === 'turbo')
|
|
349
|
+
return true;
|
|
350
|
+
if (approvalModeRef.current === 'auto-edit' && (toolName === 'write_file' || toolName === 'edit'))
|
|
351
|
+
return true;
|
|
352
|
+
if (approvalModeRef.current === 'plan')
|
|
353
|
+
return false;
|
|
354
|
+
if (exemptedToolsRef.current.has(toolName))
|
|
355
|
+
return true;
|
|
356
|
+
// Default mode — ask user for confirmation
|
|
357
|
+
return new Promise((resolve) => {
|
|
358
|
+
pendingApprovalResolveRef.current = resolve;
|
|
359
|
+
setPendingApproval({ toolName, args, resolve });
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
const finalResponse = await agentLoopRef.current.run(input, historyForModel(messages));
|
|
364
|
+
const toolHistory = agentLoopRef.current.getToolCallHistory();
|
|
365
|
+
const bundleFile = await writeExecutionBundle({
|
|
366
|
+
sessionId: sessionIdRef.current,
|
|
367
|
+
prompt: input,
|
|
368
|
+
response: finalResponse,
|
|
369
|
+
approvalMode,
|
|
370
|
+
toolCalls: toolHistory.map(toolCall => ({
|
|
371
|
+
id: toolCall.id,
|
|
372
|
+
name: toolCall.name,
|
|
373
|
+
status: toolCall.status,
|
|
374
|
+
durationMs: toolCall.durationMs,
|
|
375
|
+
error: toolCall.error,
|
|
376
|
+
result: toolCall.result,
|
|
377
|
+
})),
|
|
378
|
+
});
|
|
379
|
+
const handoffFile = await writeSessionHandoff({
|
|
380
|
+
sessionId: sessionIdRef.current,
|
|
381
|
+
prompt: input,
|
|
382
|
+
response: finalResponse,
|
|
383
|
+
approvalMode,
|
|
384
|
+
toolCalls: toolHistory.map(toolCall => ({
|
|
385
|
+
name: toolCall.name,
|
|
386
|
+
status: toolCall.status,
|
|
387
|
+
durationMs: toolCall.durationMs,
|
|
388
|
+
error: toolCall.error,
|
|
389
|
+
})),
|
|
390
|
+
});
|
|
391
|
+
await saveSession({
|
|
392
|
+
id: sessionIdRef.current,
|
|
393
|
+
messageCount: messages.length + 2,
|
|
394
|
+
toolCallCount: toolHistory.length,
|
|
395
|
+
approvalMode,
|
|
396
|
+
lastPrompt: input,
|
|
397
|
+
lastResponse: finalResponse,
|
|
398
|
+
summary: finalResponse,
|
|
399
|
+
handoffFile,
|
|
400
|
+
bundleFile,
|
|
401
|
+
});
|
|
402
|
+
// Single batch: all final UI updates at once (no await between setState calls)
|
|
403
|
+
setIsProcessing(false);
|
|
404
|
+
setStatusText(i18n.t('ready'));
|
|
405
|
+
// Convert live tool activity card to compact summary
|
|
406
|
+
if (liveToolMessageIndexRef.current >= 0 && toolHistory.length > 0) {
|
|
407
|
+
setMessages(prev => {
|
|
408
|
+
const idx = liveToolMessageIndexRef.current;
|
|
409
|
+
if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
|
|
410
|
+
const updated = [...prev];
|
|
411
|
+
updated[idx] = {
|
|
412
|
+
role: 'tool',
|
|
413
|
+
content: JSON.stringify({ type: 'tool_activity_card', toolCalls: toolHistory, status: 'compact' }),
|
|
414
|
+
};
|
|
415
|
+
return updated;
|
|
416
|
+
}
|
|
417
|
+
return prev;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
liveToolMessageIndexRef.current = -1;
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
const error = err;
|
|
424
|
+
// Don't show error on cancellation
|
|
425
|
+
if (error.name === 'AbortError' || error.message.includes('abort') || error.message.includes('cancel')) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const msg = error.message || '';
|
|
429
|
+
let friendlyMsg;
|
|
430
|
+
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
431
|
+
friendlyMsg = i18n.t('apiErrorAuth');
|
|
432
|
+
}
|
|
433
|
+
else if (msg.includes('429') || msg.includes('rate limit')) {
|
|
434
|
+
friendlyMsg = i18n.t('apiErrorRateLimit');
|
|
435
|
+
}
|
|
436
|
+
else if (/5\d{2}|server error|Service Unavailable/i.test(msg)) {
|
|
437
|
+
friendlyMsg = i18n.t('apiErrorServer');
|
|
438
|
+
}
|
|
439
|
+
else if (/ECONNRESET|ECONNREFUSED|ENOTFOUND/i.test(msg)) {
|
|
440
|
+
friendlyMsg = i18n.t('apiErrorNetwork');
|
|
441
|
+
}
|
|
442
|
+
else if (/ETIMEDOUT|timed out/i.test(msg)) {
|
|
443
|
+
friendlyMsg = i18n.t('apiErrorTimeout');
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
friendlyMsg = `${i18n.t('error')}: ${msg}`;
|
|
447
|
+
}
|
|
448
|
+
setMessages(prev => [...prev, {
|
|
449
|
+
role: 'assistant',
|
|
450
|
+
content: friendlyMsg,
|
|
451
|
+
}]);
|
|
452
|
+
const handoffFile = await writeSessionHandoff({
|
|
453
|
+
sessionId: sessionIdRef.current,
|
|
454
|
+
prompt: input,
|
|
455
|
+
error: friendlyMsg,
|
|
456
|
+
approvalMode,
|
|
457
|
+
});
|
|
458
|
+
const bundleFile = await writeExecutionBundle({
|
|
459
|
+
sessionId: sessionIdRef.current,
|
|
460
|
+
prompt: input,
|
|
461
|
+
error: friendlyMsg,
|
|
462
|
+
approvalMode,
|
|
463
|
+
});
|
|
464
|
+
await saveSession({
|
|
465
|
+
id: sessionIdRef.current,
|
|
466
|
+
messageCount: messages.length + 2,
|
|
467
|
+
approvalMode,
|
|
468
|
+
lastPrompt: input,
|
|
469
|
+
lastError: friendlyMsg,
|
|
470
|
+
summary: friendlyMsg,
|
|
471
|
+
handoffFile,
|
|
472
|
+
bundleFile,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
// Safety net: ensure UI is always reset regardless of exit path
|
|
477
|
+
setIsProcessing(false);
|
|
478
|
+
setStatusText(i18n.t('ready'));
|
|
479
|
+
// Clear any pending approval (covers error/abort exit paths)
|
|
480
|
+
if (pendingApprovalResolveRef.current) {
|
|
481
|
+
pendingApprovalResolveRef.current(false);
|
|
482
|
+
pendingApprovalResolveRef.current = null;
|
|
483
|
+
}
|
|
484
|
+
setPendingApproval(null);
|
|
485
|
+
if (abortControllerRef.current === abortController) {
|
|
486
|
+
abortControllerRef.current = null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}, [messages, isProcessing, setupStep, handleApiKeySubmit, handleSlashCommand, approvalMode, config, localApiKey]);
|
|
490
|
+
useInput((_input, key) => {
|
|
491
|
+
const step = setupStepRef.current;
|
|
492
|
+
// Ctrl+C: delegate to interactive.ts SIGINT handler.
|
|
493
|
+
// - During processing: soft cancel (via __agentSoftCancel)
|
|
494
|
+
// - During Ready: double Ctrl+C guard (first shows hint, second exits)
|
|
495
|
+
// - Never call exit() here — that would bypass the double-Ctrl+C guard.
|
|
496
|
+
if (key.ctrl && _input === 'c') {
|
|
497
|
+
if (isProcessing && abortControllerRef.current) {
|
|
498
|
+
abortControllerRef.current.abort();
|
|
499
|
+
setStatusText(i18n.t('cancelled'));
|
|
500
|
+
if (pendingApprovalResolveRef.current) {
|
|
501
|
+
pendingApprovalResolveRef.current(false);
|
|
502
|
+
pendingApprovalResolveRef.current = null;
|
|
503
|
+
}
|
|
504
|
+
setPendingApproval(null);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// When not processing: set flag so SIGINT handler can exit immediately
|
|
508
|
+
// (interactive.ts checks __pendingExit for immediate exit after agent finishes)
|
|
509
|
+
const proc = process;
|
|
510
|
+
proc.__pendingExit = true;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// When not in setup mode, let InputBar handle all keyboard input
|
|
514
|
+
if (step === 'done') {
|
|
515
|
+
// Handle clear confirmation dialog
|
|
516
|
+
if (pendingClear) {
|
|
517
|
+
if (key.upArrow) {
|
|
518
|
+
setClearCursor(prev => Math.max(0, prev - 1));
|
|
519
|
+
}
|
|
520
|
+
else if (key.downArrow) {
|
|
521
|
+
setClearCursor(prev => Math.min(1, prev + 1));
|
|
522
|
+
}
|
|
523
|
+
else if (key.return) {
|
|
524
|
+
if (clearCursor === 0)
|
|
525
|
+
executeClear();
|
|
526
|
+
else
|
|
527
|
+
setPendingClear(false);
|
|
528
|
+
}
|
|
529
|
+
else if (key.escape) {
|
|
530
|
+
setPendingClear(false);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// Handle approval dialog
|
|
535
|
+
if (pendingApproval) {
|
|
536
|
+
if (key.upArrow) {
|
|
537
|
+
setApprovalCursor(prev => Math.max(0, prev - 1));
|
|
538
|
+
}
|
|
539
|
+
else if (key.downArrow) {
|
|
540
|
+
setApprovalCursor(prev => Math.min(3, prev + 1));
|
|
541
|
+
}
|
|
542
|
+
else if (key.return) {
|
|
543
|
+
const cursor = approvalCursor;
|
|
544
|
+
const resolve = pendingApproval.resolve;
|
|
545
|
+
const toolName = pendingApproval.toolName;
|
|
546
|
+
setApprovalCursor(0);
|
|
547
|
+
setPendingApproval(null);
|
|
548
|
+
pendingApprovalResolveRef.current = null;
|
|
549
|
+
if (cursor === 0) {
|
|
550
|
+
resolve(true);
|
|
551
|
+
}
|
|
552
|
+
else if (cursor === 1) {
|
|
553
|
+
resolve(false);
|
|
554
|
+
}
|
|
555
|
+
else if (cursor === 2) {
|
|
556
|
+
exemptedToolsRef.current.add(toolName);
|
|
557
|
+
addServiceNotice(`🔇 ${toolName}: больше не спрашивать в этой сессии`);
|
|
558
|
+
resolve(true);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
setApprovalMode('turbo');
|
|
562
|
+
saveConfig({ ...config, approvalMode: 'turbo' }).catch(() => { });
|
|
563
|
+
addServiceNotice('⚡ Turbo режим включён: инструменты выполняются без подтверждения.');
|
|
564
|
+
resolve(true);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else if (key.escape) {
|
|
568
|
+
setApprovalCursor(0);
|
|
569
|
+
const resolve = pendingApproval.resolve;
|
|
570
|
+
setPendingApproval(null);
|
|
571
|
+
pendingApprovalResolveRef.current = null;
|
|
572
|
+
resolve(false);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// Tab for approval mode cycling — instant switch, no y/n confirmation
|
|
577
|
+
if (key.tab) {
|
|
578
|
+
setApprovalMode(prev => {
|
|
579
|
+
const modes = ['plan', 'default', 'auto-edit', 'turbo'];
|
|
580
|
+
const nextIdx = (modes.indexOf(prev) + 1) % modes.length;
|
|
581
|
+
const newMode = modes[nextIdx];
|
|
582
|
+
saveConfig({ ...config, approvalMode: newMode }).catch(() => { });
|
|
583
|
+
// Local warning — NOT an agent message, NOT sent to model
|
|
584
|
+
if (newMode === 'turbo') {
|
|
585
|
+
addServiceNotice('⚡ Включён режим Turbo: инструменты будут выполняться без подтверждения.');
|
|
586
|
+
}
|
|
587
|
+
else if (prev === 'turbo') {
|
|
588
|
+
addServiceNotice('Режим Turbo выключен.');
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
addServiceNotice(`Режим: ${newMode}`);
|
|
592
|
+
}
|
|
593
|
+
return newMode;
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// Scroll chat history — always works regardless of processing state.
|
|
598
|
+
// PageUp: scroll up by ~half a screen
|
|
599
|
+
if (key.pageUp) {
|
|
600
|
+
const visibleCount = messages.filter(m => m.role !== 'tool').length;
|
|
601
|
+
const next = Math.min(chatScrollOffset + 10, Math.max(0, visibleCount - 1));
|
|
602
|
+
if (next > 0)
|
|
603
|
+
setScrollMode('paused');
|
|
604
|
+
setChatScrollOffset(next);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
// PageDown: scroll down by ~half a screen
|
|
608
|
+
if (key.pageDown) {
|
|
609
|
+
const next = Math.max(0, chatScrollOffset - 10);
|
|
610
|
+
setChatScrollOffset(next);
|
|
611
|
+
if (next === 0) {
|
|
612
|
+
setScrollMode('follow');
|
|
613
|
+
setNewMessagesWhilePaused(false);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
// Theme picker: interactive selection
|
|
618
|
+
if (themePicker) {
|
|
619
|
+
if (key.escape) {
|
|
620
|
+
setThemePicker(null);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (key.upArrow) {
|
|
624
|
+
setThemePicker(prev => {
|
|
625
|
+
if (!prev)
|
|
626
|
+
return null;
|
|
627
|
+
return { ...prev, selectedIndex: Math.max(0, prev.selectedIndex - 1) };
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (key.downArrow) {
|
|
632
|
+
setThemePicker(prev => {
|
|
633
|
+
if (!prev)
|
|
634
|
+
return null;
|
|
635
|
+
return { ...prev, selectedIndex: Math.min(prev.themes.length - 1, prev.selectedIndex + 1) };
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (key.return) {
|
|
640
|
+
const picker = themePicker;
|
|
641
|
+
const chosen = picker.themes[picker.selectedIndex];
|
|
642
|
+
themeManager.setTheme(chosen.name);
|
|
643
|
+
setThemePicker(null);
|
|
644
|
+
addServiceNotice(`🎨 Тема изменена: ${chosen.name}`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Model picker: interactive selection
|
|
650
|
+
if (modelPicker) {
|
|
651
|
+
if (key.escape) {
|
|
652
|
+
setModelPicker(null);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (key.upArrow) {
|
|
656
|
+
setModelPicker(prev => prev ? { selectedIndex: Math.max(0, prev.selectedIndex - 1) } : null);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (key.downArrow) {
|
|
660
|
+
setModelPicker(prev => prev ? { selectedIndex: Math.min(DEEPSEEK_MODELS.length - 1, prev.selectedIndex + 1) } : null);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (key.return) {
|
|
664
|
+
const chosen = DEEPSEEK_MODELS[modelPicker.selectedIndex];
|
|
665
|
+
if (chosen) {
|
|
666
|
+
saveConfig({ ...config, model: chosen.id }).catch(() => { });
|
|
667
|
+
config.model = chosen.id;
|
|
668
|
+
setModelPicker(null);
|
|
669
|
+
addServiceNotice(`🤖 Модель: ${chosen.label} (${chosen.id})`);
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// ArrowUp/ArrowDown: scroll by 1 line, but only when InputBar is disabled (processing)
|
|
676
|
+
// When InputBar is active, arrows belong to input history/suggestions.
|
|
677
|
+
if (key.upArrow && isProcessing) {
|
|
678
|
+
const visibleCount = messages.filter(m => m.role !== 'tool').length;
|
|
679
|
+
const next = Math.min(chatScrollOffset + 1, Math.max(0, visibleCount - 1));
|
|
680
|
+
if (next > 0)
|
|
681
|
+
setScrollMode('paused');
|
|
682
|
+
setChatScrollOffset(next);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (key.downArrow && isProcessing) {
|
|
686
|
+
const next = Math.max(0, chatScrollOffset - 1);
|
|
687
|
+
setChatScrollOffset(next);
|
|
688
|
+
if (next === 0) {
|
|
689
|
+
setScrollMode('follow');
|
|
690
|
+
setNewMessagesWhilePaused(false);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// Step 1: Language selection — arrows + Enter + Escape
|
|
697
|
+
if (step === 'lang') {
|
|
698
|
+
if (key.escape) {
|
|
699
|
+
exit();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (key.downArrow) {
|
|
703
|
+
setLangCursor(Math.min(langCursor + 1, langOptions.length - 1));
|
|
704
|
+
}
|
|
705
|
+
else if (key.upArrow) {
|
|
706
|
+
setLangCursor(Math.max(langCursor - 1, 0));
|
|
707
|
+
}
|
|
708
|
+
else if (key.return) {
|
|
709
|
+
const lang = langOptions[langCursor];
|
|
710
|
+
i18n.setLocale(lang);
|
|
711
|
+
setSetupStep('apikey');
|
|
712
|
+
setLangCursor(0);
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// Step 2: API key — handled via InputBar, Escape to go back
|
|
717
|
+
if (step === 'apikey') {
|
|
718
|
+
if (key.escape) {
|
|
719
|
+
setSetupStep('lang');
|
|
720
|
+
setLangCursor(0);
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
// Step 3: Theme selection — arrows + Enter + Escape
|
|
725
|
+
if (step === 'theme') {
|
|
726
|
+
if (key.escape) {
|
|
727
|
+
setSetupStep('apikey');
|
|
728
|
+
setApiKeyError('');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const themes = themeManager.listThemes();
|
|
732
|
+
if (key.downArrow) {
|
|
733
|
+
const next = Math.min(themeCursor + 1, themes.length - 1);
|
|
734
|
+
themeManager.setTheme(themes[next].name);
|
|
735
|
+
setThemeCursor(next);
|
|
736
|
+
}
|
|
737
|
+
else if (key.upArrow) {
|
|
738
|
+
const prev = Math.max(themeCursor - 1, 0);
|
|
739
|
+
themeManager.setTheme(themes[prev].name);
|
|
740
|
+
setThemeCursor(prev);
|
|
741
|
+
}
|
|
742
|
+
else if (key.return) {
|
|
743
|
+
setSetupStep('model');
|
|
744
|
+
setModelCursor(0);
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
// Step 4: Model selection — arrows + Enter + Escape
|
|
749
|
+
if (step === 'model') {
|
|
750
|
+
if (key.escape) {
|
|
751
|
+
setSetupStep('theme');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (key.downArrow) {
|
|
755
|
+
setModelCursor(Math.min(modelCursor + 1, DEEPSEEK_MODELS.length - 1));
|
|
756
|
+
}
|
|
757
|
+
else if (key.upArrow) {
|
|
758
|
+
setModelCursor(Math.max(modelCursor - 1, 0));
|
|
759
|
+
}
|
|
760
|
+
else if (key.return) {
|
|
761
|
+
setSetupStep('mode');
|
|
762
|
+
setModeCursor(0);
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Step 5: Mode selection — arrows + Enter + Escape
|
|
767
|
+
if (step === 'mode') {
|
|
768
|
+
if (key.escape) {
|
|
769
|
+
setSetupStep('model');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (key.downArrow) {
|
|
773
|
+
setModeCursor(Math.min(modeCursor + 1, modeOptions.length - 1));
|
|
774
|
+
}
|
|
775
|
+
else if (key.upArrow) {
|
|
776
|
+
setModeCursor(Math.max(modeCursor - 1, 0));
|
|
777
|
+
}
|
|
778
|
+
else if (key.return) {
|
|
779
|
+
finishSetup();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
// Get context usage percent from AgentLoop metrics
|
|
784
|
+
useEffect(() => {
|
|
785
|
+
if (!isProcessing || !agentLoopRef.current) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const interval = setInterval(() => {
|
|
789
|
+
if (agentLoopRef.current) {
|
|
790
|
+
const metrics = agentLoopRef.current.getMetrics();
|
|
791
|
+
setContextPercent(metrics.getCurrentWindowPercent());
|
|
792
|
+
setTotalTokens(metrics.totalTokens);
|
|
793
|
+
setEstimatedCost(metrics.estimatedCostUSD(config.model));
|
|
794
|
+
}
|
|
795
|
+
}, 2000);
|
|
796
|
+
return () => clearInterval(interval);
|
|
797
|
+
}, [isProcessing]);
|
|
798
|
+
const executeClear = useCallback(() => {
|
|
799
|
+
setMessages([]);
|
|
800
|
+
setToolCalls([]);
|
|
801
|
+
setPendingApproval(null);
|
|
802
|
+
setPendingImage(null);
|
|
803
|
+
setScrollMode('follow');
|
|
804
|
+
setNewMessagesWhilePaused(false);
|
|
805
|
+
setChatScrollOffset(0);
|
|
806
|
+
liveToolMessageIndexRef.current = -1;
|
|
807
|
+
setServiceNotice(null);
|
|
808
|
+
if (serviceNoticeTimerRef.current) {
|
|
809
|
+
clearTimeout(serviceNoticeTimerRef.current);
|
|
810
|
+
serviceNoticeTimerRef.current = null;
|
|
811
|
+
}
|
|
812
|
+
setPendingClear(false);
|
|
813
|
+
}, []);
|
|
814
|
+
const handleClear = useCallback(() => {
|
|
815
|
+
if (messages.length === 0 && toolCalls.length === 0) {
|
|
816
|
+
executeClear();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
setPendingClear(true);
|
|
820
|
+
setClearCursor(0);
|
|
821
|
+
}, [messages.length, toolCalls.length, executeClear]);
|
|
822
|
+
const handleExit = useCallback(() => { exit(); }, [exit]);
|
|
823
|
+
const colors = themeManager.getColors();
|
|
824
|
+
return (_jsxs(Box, { flexDirection: 'column', height: '100%', children: [setupStep !== 'done'
|
|
825
|
+
? _jsx(SetupWizard, { state: {
|
|
826
|
+
step: setupStep,
|
|
827
|
+
apiKeyError,
|
|
828
|
+
langCursor,
|
|
829
|
+
themeCursor,
|
|
830
|
+
modelCursor,
|
|
831
|
+
modeCursor,
|
|
832
|
+
langOptions,
|
|
833
|
+
modeOptions,
|
|
834
|
+
} })
|
|
835
|
+
: (_jsxs(Box, { flexDirection: 'column', flexGrow: 1, children: [_jsx(Logo, {}), _jsx(ChatView, { messages: messages, scrollOffset: chatScrollOffset, hasNewMessages: newMessagesWhilePaused }), serviceNotice && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsx(Text, { color: colors.primary, children: serviceNotice }) })), themePicker && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.primary, children: [_jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "\uD83C\uDFA8 \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443" }) }), _jsx(Box, { marginLeft: 1, marginTop: 1, flexDirection: 'column', children: themePicker.themes.map((t, i) => (_jsx(Box, { children: _jsxs(Text, { color: i === themePicker.selectedIndex ? colors.primary : colors.textMuted, children: [i === themePicker.selectedIndex ? '▸ ' : ' ', t.name, t.name === themeManager.theme.name ? ' (текущая)' : ''] }) }, t.name))) }), _jsx(Box, { marginLeft: 1, marginBottom: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })), modelPicker && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.primary, children: [_jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "\uD83E\uDD16 \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043C\u043E\u0434\u0435\u043B\u044C" }) }), _jsx(Box, { marginLeft: 1, marginTop: 1, flexDirection: 'column', children: DEEPSEEK_MODELS.map((m, i) => (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { color: i === modelPicker.selectedIndex ? colors.primary : colors.textMuted, children: [i === modelPicker.selectedIndex ? '▸ ' : ' ', _jsx(Text, { bold: i === modelPicker.selectedIndex, children: m.label }), m.id === config.model ? _jsx(Text, { dimColor: true, children: " (\u0442\u0435\u043A\u0443\u0449\u0430\u044F)" }) : null] }), _jsxs(Text, { dimColor: true, children: [' ', m.description] })] }, m.id))) }), _jsx(Box, { marginLeft: 1, marginBottom: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })), pendingApproval && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.warning, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: colors.warning, children: "\uD83D\uDD14 \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0432\u044B\u0437\u043E\u0432 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430?" }) }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { bold: true, color: colors.text, children: pendingApproval.toolName }) }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.textMuted, children: JSON.stringify(pendingApproval.args, null, 2).slice(0, 200) }) }), _jsxs(Box, { flexDirection: 'column', marginTop: 1, children: [[
|
|
836
|
+
'✅ Подтвердить',
|
|
837
|
+
'❌ Отклонить',
|
|
838
|
+
`🔇 Не спрашивать для "${pendingApproval.toolName}"`,
|
|
839
|
+
'⚡ Turbo — выполнять всё без вопросов',
|
|
840
|
+
].map((label, i) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: approvalCursor === i ? colors.primary : colors.text, children: [approvalCursor === i ? '❯ ' : ' ', label] }) }, i))), _jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C Esc \u2014 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C" }) })] })] })), pendingClear && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.warning, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: colors.warning, children: "\u26A0\uFE0F \u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0447\u0430\u0442\u0430?" }) }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: colors.textMuted, children: [messages.length, " \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043B\u0435\u043D\u043E. \u041E\u0442\u043C\u0435\u043D\u0443 \u043D\u0435\u043B\u044C\u0437\u044F."] }) }), _jsxs(Box, { flexDirection: 'column', marginTop: 1, children: [['✅ Да, очистить', '❌ Отмена'].map((label, i) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: clearCursor === i ? colors.primary : colors.text, children: [clearCursor === i ? '❯ ' : ' ', label] }) }, i))), _jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })] }))] })), _jsx(InputBar, { onSubmit: handleSubmit, disabled: isProcessing, onClear: handleClear, onExit: handleExit, isMasked: setupStep === 'apikey', isSetupMode: setupStep !== 'done', blockInput: setupStep === 'done' && (pendingApproval !== null || pendingClear), emptyHint: emptyInputHint, onImagePaste: (base64, mimeType) => {
|
|
841
|
+
const model = config.model ?? '';
|
|
842
|
+
if (!model.includes('vl') && !model.includes('vision')) {
|
|
843
|
+
setMessages(prev => [...prev, {
|
|
844
|
+
role: 'assistant',
|
|
845
|
+
content: `⚠️ Вставка изображения требует модель с поддержкой vision.\nТекущая модель: ${model || 'неизвестно'}\nИспользуйте модель с "vl" или "vision" в названии.`,
|
|
846
|
+
}]);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
setPendingImage({ base64, mimeType });
|
|
850
|
+
} }), _jsx(StatusBar, { mode: approvalMode, status: statusText, messageCount: messages.length, isProcessing: isProcessing, contextPercent: contextPercent, totalTokens: totalTokens, estimatedCost: estimatedCost, model: config.model })] }));
|
|
851
|
+
}
|
|
852
|
+
//# sourceMappingURL=app.js.map
|