scalix 1.0.0 → 1.2.0
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/.env.example +29 -0
- package/PARITY-PLAN.md +116 -0
- package/README.md +1 -1
- package/bin/scalix.js +1 -0
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent.js +363 -74
- package/dist/api-client.js +77 -0
- package/dist/cli.js +724 -76
- package/dist/commands.js +53 -2
- package/dist/constants.js +43 -9
- package/dist/context-builder.js +8 -1
- package/dist/context-retriever.js +257 -0
- package/dist/git-context.js +32 -29
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +149 -20
- package/dist/hooks.js +148 -10
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +6 -2
- package/dist/keychain.js +109 -0
- package/dist/markdown.js +46 -3
- package/dist/plugins.js +132 -0
- package/dist/project-index.js +22 -1
- package/dist/sandbox.js +201 -11
- package/dist/sessions.js +96 -2
- package/dist/settings.js +194 -8
- package/dist/skills.js +269 -0
- package/dist/sub-agent.js +83 -8
- package/dist/task-board.js +105 -0
- package/dist/terminal-notify.js +73 -0
- package/dist/text-utils.js +146 -0
- package/dist/tools-advanced.js +304 -49
- package/dist/tools-browser.js +280 -19
- package/dist/tools-extra.js +9 -8
- package/dist/tools-vision.js +4 -3
- package/dist/tools.js +13 -3
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/package.json +8 -3
- package/app.py +0 -55
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback, useRef, useMemo, memo } from 'react';
|
|
3
|
-
import { render, Box, Text,
|
|
3
|
+
import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
5
|
import TextInput from 'ink-text-input';
|
|
6
6
|
import Gradient from 'ink-gradient';
|
|
7
7
|
import BigText from 'ink-big-text';
|
|
8
8
|
import { execSync, execFileSync } from 'node:child_process';
|
|
9
|
+
import os from 'node:os';
|
|
9
10
|
import fs from 'node:fs/promises';
|
|
10
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
@@ -17,31 +18,36 @@ import { webTools } from './tools-web.js';
|
|
|
17
18
|
import { advancedTools } from './tools-advanced.js';
|
|
18
19
|
import { visionTools } from './tools-vision.js';
|
|
19
20
|
import { voiceTools } from './tools-voice.js';
|
|
20
|
-
import { browserTools } from './tools-browser.js';
|
|
21
|
+
import { browserTools, closeBrowser } from './tools-browser.js';
|
|
21
22
|
import { loadMcpServices } from './mcp.js';
|
|
22
23
|
import { renderMarkdown } from './markdown.js';
|
|
23
24
|
import { costTracker } from './cost-tracker.js';
|
|
24
|
-
import { saveSession, listSessions, loadSession, generateSessionId } from './sessions.js';
|
|
25
|
+
import { saveSession, listSessions, loadSession, generateSessionId, saveCrashState, loadCrashState, clearCrashState, forkSession } from './sessions.js';
|
|
25
26
|
import { logger } from './logger.js';
|
|
26
|
-
import { VERSION, PRODUCT_NAME, DEFAULT_MODELS, DEFAULT_BASE_URL, DEFAULT_INSTRUCTIONS, ENV_API_KEY, ENV_MODEL, TOOL_DEFAULTS, HEADLESS_DEFAULTS, fetchModelSpecs } from './constants.js';
|
|
27
|
+
import { VERSION, PRODUCT_NAME, DEFAULT_MODELS, DEFAULT_BASE_URL, DEFAULT_INSTRUCTIONS, ENV_API_KEY, ENV_MODEL, TOOL_DEFAULTS, HEADLESS_DEFAULTS, GIT_BOT_IDENTITY, fetchModelSpecs } from './constants.js';
|
|
28
|
+
import { checkForUpdate, getUpdateBanner, performUpdate } from './update-checker.js';
|
|
27
29
|
import { loadConfig, saveConfig, isConfigured, DEFAULT_LIMITS, THINKING_MODES } from './settings.js';
|
|
28
|
-
import { getModeDescription, PERMISSION_MODES, setSandboxRoot } from './sandbox.js';
|
|
30
|
+
import { getModeDescription, PERMISSION_MODES, setSandboxRoot, checkScopedPermission, getScopedPermissionRules } from './sandbox.js';
|
|
29
31
|
import { buildProjectIndex, indexToContext } from './project-index.js';
|
|
30
32
|
import { collectGitInfo, gitInfoToContext } from './git-context.js';
|
|
31
33
|
import { loadMemory, autoDetectConventions, saveMemory, memoryToContext, addNote, detectCorrection } from './project-memory.js';
|
|
32
|
-
import { discoverPlugins, getPluginTools, pluginsSummary } from './plugins.js';
|
|
34
|
+
import { discoverPlugins, getPluginTools, pluginsSummary, installPlugin, uninstallPlugin, listInstalledPlugins, enablePlugin, disablePlugin } from './plugins.js';
|
|
33
35
|
import { createRouterConfig, getRouterStatus } from './model-router.js';
|
|
34
36
|
import { loadCustomCommands, expandCommand, commandsSummary } from './commands.js';
|
|
35
37
|
import { hooksSummary } from './hooks.js';
|
|
36
38
|
import { spawnSubAgent, pickCheapestModel } from './sub-agent.js';
|
|
37
39
|
import { createCheckpoint, undoLastCheckpoint, listCheckpoints } from './git-undo.js';
|
|
38
40
|
import { loadScopedRules, rulesToContext } from './scoped-rules.js';
|
|
41
|
+
import { loadSkills, skillsSummary, getSkillByTrigger, getSkillContent } from './skills.js';
|
|
42
|
+
import { AgentRegistry } from './agent-registry.js';
|
|
43
|
+
import { createWorktree, listWorktrees, removeWorktree, cleanupStaleWorktrees } from './worktree.js';
|
|
44
|
+
import { sendNotification, getTerminalType } from './terminal-notify.js';
|
|
39
45
|
logger.setLevel('silent');
|
|
40
46
|
setSandboxRoot(process.cwd());
|
|
41
47
|
// Suppress noisy React 19 development-mode reconciler warnings that leak to stderr
|
|
42
48
|
const _origConsoleError = console.error;
|
|
43
49
|
console.error = (...args) => {
|
|
44
|
-
if (typeof args[0] === 'string' && args[0]
|
|
50
|
+
if (typeof args[0] === 'string' && args[0].startsWith('Warning:') && args[0].includes('same key'))
|
|
45
51
|
return;
|
|
46
52
|
_origConsoleError(...args);
|
|
47
53
|
};
|
|
@@ -55,10 +61,21 @@ const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools,
|
|
|
55
61
|
import { EventEmitter } from 'node:events';
|
|
56
62
|
const confirmEmitter = new EventEmitter();
|
|
57
63
|
let pendingConfirm = null;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
const alwaysAllowedTools = new Set();
|
|
65
|
+
const recentDenials = [];
|
|
66
|
+
const confirmToolCall = async (toolName, args) => {
|
|
67
|
+
if (alwaysAllowedTools.has(toolName))
|
|
68
|
+
return true;
|
|
69
|
+
const scoped = checkScopedPermission(toolName, args);
|
|
70
|
+
if (scoped === 'allow')
|
|
71
|
+
return true;
|
|
72
|
+
if (scoped === 'deny')
|
|
73
|
+
return false;
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
pendingConfirm = { resolve, toolName, args };
|
|
76
|
+
confirmEmitter.emit('pending');
|
|
77
|
+
});
|
|
78
|
+
};
|
|
62
79
|
// ─── Agent (deferred until config is ready) ───
|
|
63
80
|
let baseInstructions = DEFAULT_INSTRUCTIONS;
|
|
64
81
|
let agent = null;
|
|
@@ -67,11 +84,12 @@ let projectIndex = null;
|
|
|
67
84
|
let loadedPlugins = [];
|
|
68
85
|
let projectMemory = null;
|
|
69
86
|
let customCommands = [];
|
|
87
|
+
let loadedSkills = [];
|
|
70
88
|
async function buildEnhancedInstructions() {
|
|
71
89
|
let instructions = baseInstructions;
|
|
72
90
|
const cwd = process.cwd();
|
|
73
91
|
try {
|
|
74
|
-
const gitInfo = collectGitInfo(cwd);
|
|
92
|
+
const gitInfo = await collectGitInfo(cwd);
|
|
75
93
|
if (gitInfo.isRepo)
|
|
76
94
|
instructions += '\n\n' + gitInfoToContext(gitInfo);
|
|
77
95
|
}
|
|
@@ -151,7 +169,10 @@ function initAgent(config) {
|
|
|
151
169
|
});
|
|
152
170
|
loadCustomCommands(process.cwd()).then((cmds) => {
|
|
153
171
|
customCommands = cmds;
|
|
154
|
-
}).catch(() => { });
|
|
172
|
+
}).catch((err) => { logger.warn('Failed to load custom commands', { error: err.message }); });
|
|
173
|
+
loadSkills(process.cwd()).then((skills) => {
|
|
174
|
+
loadedSkills = skills;
|
|
175
|
+
}).catch((err) => { logger.warn('Failed to load skills', { error: err.message }); });
|
|
155
176
|
return agent;
|
|
156
177
|
}
|
|
157
178
|
// ─── Helpers ───
|
|
@@ -202,6 +223,43 @@ const Banner = memo(function Banner() {
|
|
|
202
223
|
function sanitizeInput(v) {
|
|
203
224
|
return v.replace(/\[20[01]~/g, '').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
|
204
225
|
}
|
|
226
|
+
async function tryPasteImage() {
|
|
227
|
+
const platform = os.platform();
|
|
228
|
+
const tmpFile = path.join(os.tmpdir(), `scalix-paste-${Date.now()}.png`);
|
|
229
|
+
try {
|
|
230
|
+
if (platform === 'darwin') {
|
|
231
|
+
try {
|
|
232
|
+
execSync(`pngpaste ${tmpFile}`, { stdio: 'pipe', timeout: 3000 });
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
const hasImage = execSync(`osascript -e 'clipboard info' 2>/dev/null | grep -q 'TIFF\\|PNG\\|JPEG' && echo yes || echo no`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
236
|
+
if (hasImage !== 'yes')
|
|
237
|
+
return null;
|
|
238
|
+
execSync(`osascript -e 'set imgData to (the clipboard as «class PNGf»)' -e 'set fp to open for access POSIX file "${tmpFile}" with write permission' -e 'write imgData to fp' -e 'close access fp'`, { stdio: 'pipe', timeout: 5000 });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
execSync(`xclip -selection clipboard -t image/png -o > ${tmpFile}`, { stdio: 'pipe', timeout: 3000 });
|
|
243
|
+
}
|
|
244
|
+
const { readFileSync, unlinkSync } = await import('node:fs');
|
|
245
|
+
const data = readFileSync(tmpFile);
|
|
246
|
+
try {
|
|
247
|
+
unlinkSync(tmpFile);
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
if (data.length < 8)
|
|
251
|
+
return null;
|
|
252
|
+
return `data:image/png;base64,${data.toString('base64')}`;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
try {
|
|
256
|
+
const { unlinkSync } = await import('node:fs');
|
|
257
|
+
unlinkSync(tmpFile);
|
|
258
|
+
}
|
|
259
|
+
catch { }
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
205
263
|
function LongTextInput({ value, onChange, onSubmit, placeholder, maxVisible = 80 }) {
|
|
206
264
|
const valueRef = useRef(value);
|
|
207
265
|
useEffect(() => { valueRef.current = value; }, [value]);
|
|
@@ -300,7 +358,7 @@ function SetupScreen({ onComplete, existingConfig }) {
|
|
|
300
358
|
if (result.length > 0) {
|
|
301
359
|
setFetchedModels(result.map((m) => ({ id: m.id, name: m.name })));
|
|
302
360
|
}
|
|
303
|
-
}).catch(() => { }).finally(() => {
|
|
361
|
+
}).catch((err) => { logger.warn('Failed to fetch model specs', { error: err.message }); }).finally(() => {
|
|
304
362
|
setFetchingModels(false);
|
|
305
363
|
setStep('models');
|
|
306
364
|
});
|
|
@@ -375,6 +433,8 @@ function SetupScreen({ onComplete, existingConfig }) {
|
|
|
375
433
|
limits,
|
|
376
434
|
permissionMode: existingConfig?.permissionMode || 'default',
|
|
377
435
|
thinkingMode: existingConfig?.thinkingMode || 'off',
|
|
436
|
+
security: existingConfig?.security || {},
|
|
437
|
+
injectionDetection: existingConfig?.injectionDetection || { enabled: true, sensitivity: 'medium' },
|
|
378
438
|
};
|
|
379
439
|
saveConfig(newConfig).then(() => {
|
|
380
440
|
onComplete(newConfig);
|
|
@@ -383,7 +443,7 @@ function SetupScreen({ onComplete, existingConfig }) {
|
|
|
383
443
|
});
|
|
384
444
|
}, placeholder: String(DEFAULT_LIMITS.maxContextTokens) })] })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, italic: true, children: [stepNumber > 1 ? 'Backspace (empty) to go back \u2022 ' : '', "ESC to quit"] }) })] })] }));
|
|
385
445
|
}
|
|
386
|
-
const StatusBar = memo(function StatusBar({ status, model, toolCount, thinkStart, tokens, mode }) {
|
|
446
|
+
const StatusBar = memo(function StatusBar({ status, model, toolCount, thinkStart, tokens, mode, contextPercent, rateLimitWarn }) {
|
|
387
447
|
const { cols } = useTermSize();
|
|
388
448
|
const [elapsedStr, setElapsedStr] = useState('');
|
|
389
449
|
useEffect(() => {
|
|
@@ -398,15 +458,21 @@ const StatusBar = memo(function StatusBar({ status, model, toolCount, thinkStart
|
|
|
398
458
|
const modeColor = mode === 'yolo' ? 'red' : mode === 'auto' ? 'yellow' : mode === 'plan' ? 'blue' : 'green';
|
|
399
459
|
const statusIcon = status === 'thinking' ? '\u25D4' : status === 'error' ? '\u2718' : '\u25CF';
|
|
400
460
|
const statusColor = status === 'thinking' ? 'yellow' : status === 'error' ? 'red' : 'green';
|
|
461
|
+
const activeAgents = AgentRegistry.getInstance().listActive().length;
|
|
401
462
|
const leftParts = [];
|
|
402
463
|
leftParts.push(shortModel(model));
|
|
403
464
|
if (tokens > 0)
|
|
404
465
|
leftParts.push(`${formatTokens(tokens)} tok`);
|
|
405
466
|
leftParts.push(`${toolCount} tools`);
|
|
467
|
+
if (activeAgents > 0)
|
|
468
|
+
leftParts.push(`${activeAgents} agent${activeAgents > 1 ? 's' : ''}`);
|
|
469
|
+
if (contextPercent > 50)
|
|
470
|
+
leftParts.push(`ctx ${contextPercent}%`);
|
|
406
471
|
const leftText = leftParts.join(' \u00B7 ');
|
|
407
|
-
const
|
|
472
|
+
const warnText = rateLimitWarn ? ' \u26A0 Rate limited' : '';
|
|
473
|
+
const rightLen = mode.length + VERSION.length + 6 + (elapsedStr ? elapsedStr.length + 3 : 0) + warnText.length;
|
|
408
474
|
const gap = Math.max(cols - leftText.length - rightLen - 8, 2);
|
|
409
|
-
return (_jsxs(Box, { paddingX: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: statusColor, children: statusIcon }), _jsx(Text, { dimColor: true, children: leftText })] }), _jsx(Text, { children: ' '.repeat(gap) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: modeColor, bold: true, children: mode }), elapsedStr ? _jsxs(Text, { dimColor: true, children: ['\u00B7', " ", elapsedStr] }) : null, _jsxs(Text, { dimColor: true, children: ['\u00B7', " v", VERSION] })] })] }));
|
|
475
|
+
return (_jsxs(Box, { paddingX: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: statusColor, children: statusIcon }), _jsx(Text, { dimColor: true, children: leftText })] }), _jsx(Text, { children: ' '.repeat(gap) }), _jsxs(Box, { gap: 1, children: [rateLimitWarn && _jsxs(Text, { color: "red", bold: true, children: ['\u26A0', " Rate limited"] }), _jsx(Text, { color: modeColor, bold: true, children: mode }), elapsedStr ? _jsxs(Text, { dimColor: true, children: ['\u00B7', " ", elapsedStr] }) : null, _jsxs(Text, { dimColor: true, children: ['\u00B7', " v", VERSION] })] })] }));
|
|
410
476
|
});
|
|
411
477
|
const WelcomeScreen = memo(function WelcomeScreen() {
|
|
412
478
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 0, children: [_jsx(Text, { dimColor: true, children: " Type a message or use /help for commands. Press Esc to exit." }), _jsxs(Box, { marginTop: 0, paddingLeft: 2, gap: 1, flexWrap: "wrap", children: [_jsx(Text, { dimColor: true, children: "Tools:" }), ['shell', 'read', 'write', 'edit', 'search', 'find', 'git', 'web', 'tree'].map((t) => (_jsx(Text, { dimColor: true, bold: true, children: t }, t)))] })] }));
|
|
@@ -449,6 +515,12 @@ const ToolResultMessage = memo(function ToolResultMessage({ name, result }) {
|
|
|
449
515
|
const ToolBlockedMessage = memo(function ToolBlockedMessage({ text }) {
|
|
450
516
|
return (_jsxs(Box, { paddingLeft: 3, paddingRight: 2, gap: 1, children: [_jsx(Text, { color: "red", children: '\u25CF' }), _jsx(Text, { color: "red", children: text })] }));
|
|
451
517
|
});
|
|
518
|
+
const ToolGroupMessage = memo(function ToolGroupMessage({ count, children, collapsed }) {
|
|
519
|
+
if (collapsed || !children) {
|
|
520
|
+
return (_jsxs(Box, { paddingLeft: 3, paddingRight: 2, gap: 1, marginTop: 1, children: [_jsx(Text, { color: "cyan", children: '○' }), _jsxs(Text, { dimColor: true, children: ["Ran ", count, " tools"] }), _jsx(Text, { dimColor: true, italic: true, children: "(Ctrl+O to expand)" })] }));
|
|
521
|
+
}
|
|
522
|
+
return (_jsx(Box, { flexDirection: "column", children: children.map(child => _jsx(RenderItem, { item: child }, child.id)) }));
|
|
523
|
+
});
|
|
452
524
|
const DIFF_MAX_LINES = 60;
|
|
453
525
|
const DiffMessage = memo(function DiffMessage({ text, meta }) {
|
|
454
526
|
const allLines = text.split('\n');
|
|
@@ -484,6 +556,7 @@ const RenderItem = memo(function RenderItem({ item }) {
|
|
|
484
556
|
case 'tool-call': return _jsx(ToolCallMessage, { name: item.meta ?? '', args: item.text });
|
|
485
557
|
case 'tool-result': return _jsx(ToolResultMessage, { name: item.meta ?? '', result: item.text });
|
|
486
558
|
case 'tool-blocked': return _jsx(ToolBlockedMessage, { text: item.text });
|
|
559
|
+
case 'tool-group': return _jsx(ToolGroupMessage, { count: item.children?.length ?? 0, children: item.children, collapsed: item.collapsed });
|
|
487
560
|
case 'diff': return _jsx(DiffMessage, { text: item.text, meta: item.meta });
|
|
488
561
|
case 'system': return _jsx(SystemMessage, { text: item.text });
|
|
489
562
|
}
|
|
@@ -510,18 +583,43 @@ const StreamingArea = memo(function StreamingArea({ text, reasoning, model, maxL
|
|
|
510
583
|
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 3, paddingRight: 2, marginTop: 1, children: [displayReasoning.length > 0 && (_jsx(Box, { paddingLeft: 0, marginBottom: 0, children: _jsxs(Text, { color: "magenta", dimColor: true, italic: true, wrap: "wrap", children: ['\u2502', " ", displayReasoning] }) })), displayText.length > 0 ? (_jsxs(Text, { wrap: "wrap", children: [renderMarkdown(displayText), _jsx(Text, { color: "magentaBright", children: '\u258E' })] })) : displayReasoning.length === 0 ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "magentaBright", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { dimColor: true, children: ["Thinking", model ? ` (${model})` : '', "..."] })] })) : null] }));
|
|
511
584
|
});
|
|
512
585
|
function ConfirmPrompt({ toolName, args, onRespond, onScrollUp, onScrollDown }) {
|
|
586
|
+
const { cols } = useTermSize();
|
|
513
587
|
useInput((ch, key) => {
|
|
514
|
-
if (ch.toLowerCase() === '
|
|
588
|
+
if (ch.toLowerCase() === 'a')
|
|
589
|
+
onRespond(true, true);
|
|
590
|
+
else if (ch.toLowerCase() === 'y')
|
|
515
591
|
onRespond(true);
|
|
516
592
|
else if (ch.toLowerCase() === 'n' || key.escape)
|
|
517
593
|
onRespond(false);
|
|
518
|
-
else if (key.pageUp
|
|
519
|
-
onScrollUp?.();
|
|
520
|
-
else if (key.
|
|
521
|
-
|
|
594
|
+
else if (key.pageUp)
|
|
595
|
+
onScrollUp?.(15);
|
|
596
|
+
else if (key.upArrow && key.shift)
|
|
597
|
+
onScrollUp?.(5);
|
|
598
|
+
else if (key.pageDown)
|
|
599
|
+
onScrollDown?.(15);
|
|
600
|
+
else if (key.downArrow && key.shift)
|
|
601
|
+
onScrollDown?.(5);
|
|
522
602
|
});
|
|
523
|
-
|
|
524
|
-
|
|
603
|
+
let parsed = {};
|
|
604
|
+
try {
|
|
605
|
+
parsed = typeof args === 'string' ? JSON.parse(args) : args ?? {};
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
parsed = {};
|
|
609
|
+
}
|
|
610
|
+
const isCommand = toolName === 'run_command';
|
|
611
|
+
const isWrite = toolName === 'write_file' || toolName === 'edit_file' || toolName === 'notebook_edit';
|
|
612
|
+
const isGit = toolName === 'git_commit';
|
|
613
|
+
const highlight = parsed.command ?? parsed.filePath ?? parsed.file_path ?? parsed.notebook_path ?? parsed.message ?? null;
|
|
614
|
+
const highlightLabel = isCommand ? 'Command' : isGit ? 'Message' : isWrite ? 'File' : 'Args';
|
|
615
|
+
const highlightStr = highlight ? String(highlight) : null;
|
|
616
|
+
const extraFields = Object.entries(parsed).filter(([k]) => !['command', 'filePath', 'file_path', 'notebook_path', 'message', 'content'].includes(k));
|
|
617
|
+
const boxWidth = Math.min(cols - 2, 80);
|
|
618
|
+
const border = '\u2500'.repeat(boxWidth - 2);
|
|
619
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: ['\u250C', border, '\u2510'] }), _jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsxs(Text, { color: "yellow", bold: true, children: ['\u26A0', " Permission Required"] }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - 24)) }), '\u2502'] }), _jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsx(Text, { dimColor: true, children: "Tool:" }), " ", _jsx(Text, { color: "cyan", bold: true, children: toolName }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - 9 - toolName.length)) }), '\u2502'] }), highlightStr && (_jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsxs(Text, { dimColor: true, children: [highlightLabel, ":"] }), " ", _jsx(Text, { color: "white", bold: true, children: truncate(highlightStr, boxWidth - highlightLabel.length - 6) }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - highlightLabel.length - 4 - Math.min(highlightStr.length, boxWidth - highlightLabel.length - 6))) }), '\u2502'] })), extraFields.slice(0, 3).map(([k, v]) => {
|
|
620
|
+
const val = truncate(String(v), boxWidth - k.length - 6);
|
|
621
|
+
return (_jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsxs(Text, { dimColor: true, children: [k, ":"] }), " ", _jsx(Text, { children: val }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - k.length - 4 - val.length)) }), '\u2502'] }, k));
|
|
622
|
+
}), _jsxs(Text, { color: "yellow", children: ['\u251C', border, '\u2524'] }), _jsxs(Text, { color: "yellow", children: ['\u2502', " ", _jsx(Text, { color: "green", bold: true, children: "[Y]" }), _jsx(Text, { children: " Allow " }), _jsx(Text, { color: "cyan", bold: true, children: "[A]" }), _jsx(Text, { children: " Always " }), _jsx(Text, { color: "red", bold: true, children: "[N]" }), _jsx(Text, { children: " Deny " }), _jsx(Text, { dimColor: true, children: "ESC deny" }), _jsx(Text, { children: ' '.repeat(Math.max(0, boxWidth - 42)) }), '\u2502'] }), _jsxs(Text, { color: "yellow", children: ['\u2514', border, '\u2518'] })] }));
|
|
525
623
|
}
|
|
526
624
|
function ModelPicker({ currentModels, onSelect, onCancel }) {
|
|
527
625
|
const [customInput, setCustomInput] = useState('');
|
|
@@ -607,7 +705,7 @@ function SettingsEditor({ onSave, onClose }) {
|
|
|
607
705
|
const [selectedKey, setSelectedKey] = useState(null);
|
|
608
706
|
const [editValue, setEditValue] = useState('');
|
|
609
707
|
const [currentConfig, setCurrentConfig] = useState(null);
|
|
610
|
-
const reload = useCallback(() => { loadConfig().then(setCurrentConfig).catch(() => { }); }, []);
|
|
708
|
+
const reload = useCallback(() => { loadConfig().then(setCurrentConfig).catch((err) => { logger.warn('Failed to load config', { error: err.message }); }); }, []);
|
|
611
709
|
useEffect(() => { reload(); }, [reload]);
|
|
612
710
|
useInput((_ch, key) => {
|
|
613
711
|
if (key.escape) {
|
|
@@ -707,7 +805,7 @@ function SettingsEditor({ onSave, onClose }) {
|
|
|
707
805
|
}
|
|
708
806
|
} })] }));
|
|
709
807
|
}
|
|
710
|
-
const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, onClear, onScrollUp, onScrollDown, onHeightChange }) {
|
|
808
|
+
const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, onClear, onScrollUp, onScrollDown, onHeightChange, onToggleToolGroups, onImagePaste }) {
|
|
711
809
|
const { exit } = useApp();
|
|
712
810
|
const { cols } = useTermSize();
|
|
713
811
|
const line = useMemo(() => '\u2500'.repeat(Math.max(cols - 6, 20)), [cols]);
|
|
@@ -718,7 +816,11 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
718
816
|
const [history, setHistory] = useState([]);
|
|
719
817
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
720
818
|
const savedInput = useRef('');
|
|
819
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
820
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
821
|
+
const ctrlXPending = useRef(false);
|
|
721
822
|
const valueRef = useRef('');
|
|
823
|
+
const [imageChips, setImageChips] = useState([]);
|
|
722
824
|
const onChange = useCallback((v) => {
|
|
723
825
|
setValue(v);
|
|
724
826
|
valueRef.current = v;
|
|
@@ -788,6 +890,88 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
788
890
|
onChange('');
|
|
789
891
|
return;
|
|
790
892
|
}
|
|
893
|
+
// ── Alt+Enter / Meta+Enter: insert newline ──
|
|
894
|
+
if (key.return && key.meta && !disabled) {
|
|
895
|
+
onChange(val + '\n');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
// ── Ctrl+X Ctrl+E: open external editor ──
|
|
899
|
+
if (ch === 'x' && key.ctrl && !disabled) {
|
|
900
|
+
ctrlXPending.current = true;
|
|
901
|
+
setTimeout(() => { ctrlXPending.current = false; }, 1000);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (ch === 'e' && key.ctrl && ctrlXPending.current && !disabled) {
|
|
905
|
+
ctrlXPending.current = false;
|
|
906
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
907
|
+
const tmpFile = path.join(os.tmpdir(), `scalix-input-${Date.now()}.txt`);
|
|
908
|
+
import('node:fs').then(async (fs) => {
|
|
909
|
+
fs.writeFileSync(tmpFile, val);
|
|
910
|
+
const { execSync } = await import('node:child_process');
|
|
911
|
+
try {
|
|
912
|
+
execSync(`${editor} ${tmpFile}`, { stdio: 'inherit' });
|
|
913
|
+
const edited = fs.readFileSync(tmpFile, 'utf-8');
|
|
914
|
+
onChange(edited.trimEnd());
|
|
915
|
+
}
|
|
916
|
+
catch { /* editor exited with error */ }
|
|
917
|
+
try {
|
|
918
|
+
fs.unlinkSync(tmpFile);
|
|
919
|
+
}
|
|
920
|
+
catch { /* cleanup */ }
|
|
921
|
+
});
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
// ── Ctrl+O: toggle collapsed tool groups ──
|
|
925
|
+
if (ch === 'o' && key.ctrl) {
|
|
926
|
+
onToggleToolGroups?.();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
// ── Ctrl+R: reverse history search ──
|
|
930
|
+
if (ch === 'r' && key.ctrl && !disabled) {
|
|
931
|
+
if (searchMode) {
|
|
932
|
+
setSearchMode(false);
|
|
933
|
+
setSearchQuery('');
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
setSearchMode(true);
|
|
937
|
+
setSearchQuery('');
|
|
938
|
+
}
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
// ── Search mode input handling ──
|
|
942
|
+
if (searchMode && !disabled) {
|
|
943
|
+
if (key.return) {
|
|
944
|
+
// Accept found match
|
|
945
|
+
setSearchMode(false);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (key.escape) {
|
|
949
|
+
setSearchMode(false);
|
|
950
|
+
setSearchQuery('');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (key.backspace || key.delete) {
|
|
954
|
+
const newQ = searchQuery.slice(0, -1);
|
|
955
|
+
setSearchQuery(newQ);
|
|
956
|
+
const match = history.slice().reverse().find(h => h.toLowerCase().includes(newQ.toLowerCase()));
|
|
957
|
+
if (match) {
|
|
958
|
+
setValue(match);
|
|
959
|
+
valueRef.current = match;
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (!key.ctrl && !key.meta && !key.tab && ch.length > 0) {
|
|
964
|
+
const newQ = searchQuery + ch;
|
|
965
|
+
setSearchQuery(newQ);
|
|
966
|
+
const match = history.slice().reverse().find(h => h.toLowerCase().includes(newQ.toLowerCase()));
|
|
967
|
+
if (match) {
|
|
968
|
+
setValue(match);
|
|
969
|
+
valueRef.current = match;
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
791
975
|
// ── Ctrl+S: stash / restore prompt ──
|
|
792
976
|
if (ch === 's' && key.ctrl && !disabled) {
|
|
793
977
|
if (val.length > 0) {
|
|
@@ -800,6 +984,16 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
800
984
|
}
|
|
801
985
|
return;
|
|
802
986
|
}
|
|
987
|
+
// ── Ctrl+V: paste image from clipboard ──
|
|
988
|
+
if (ch === 'v' && key.ctrl && !disabled) {
|
|
989
|
+
tryPasteImage().then(dataUrl => {
|
|
990
|
+
if (dataUrl) {
|
|
991
|
+
setImageChips(prev => [...prev, dataUrl]);
|
|
992
|
+
onImagePaste?.(dataUrl);
|
|
993
|
+
}
|
|
994
|
+
}).catch(() => { });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
803
997
|
// ── Tab: autocomplete slash commands ──
|
|
804
998
|
if (key.tab && !disabled && val.startsWith('/') && !val.includes(' ')) {
|
|
805
999
|
const cmdNames = commands.map((c) => c.cmd);
|
|
@@ -833,12 +1027,20 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
833
1027
|
return;
|
|
834
1028
|
}
|
|
835
1029
|
// ── Scroll ──
|
|
836
|
-
if (key.pageUp
|
|
837
|
-
onScrollUp();
|
|
1030
|
+
if (key.pageUp) {
|
|
1031
|
+
onScrollUp(15);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (key.upArrow && key.shift) {
|
|
1035
|
+
onScrollUp(5);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (key.pageDown) {
|
|
1039
|
+
onScrollDown(15);
|
|
838
1040
|
return;
|
|
839
1041
|
}
|
|
840
|
-
if (key.
|
|
841
|
-
onScrollDown();
|
|
1042
|
+
if (key.downArrow && key.shift) {
|
|
1043
|
+
onScrollDown(5);
|
|
842
1044
|
return;
|
|
843
1045
|
}
|
|
844
1046
|
// ── History navigation ──
|
|
@@ -880,6 +1082,8 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
880
1082
|
{ cmd: '/copy', desc: 'Copy last response to clipboard' },
|
|
881
1083
|
{ cmd: '/save', desc: 'Save current session' },
|
|
882
1084
|
{ cmd: '/load', desc: 'Resume a saved session' },
|
|
1085
|
+
{ cmd: '/resume', desc: 'Resume from crash recovery or saved session' },
|
|
1086
|
+
{ cmd: '/fork', desc: 'Fork conversation from a point in history' },
|
|
883
1087
|
{ cmd: '/export', desc: 'Export conversation as text' },
|
|
884
1088
|
{ cmd: '/index', desc: 'Scan & index project files' },
|
|
885
1089
|
{ cmd: '/init', desc: 'Initialize project config (SCALIX.md)' },
|
|
@@ -894,10 +1098,13 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
894
1098
|
{ cmd: '/mcp', desc: 'Add MCP server' },
|
|
895
1099
|
{ cmd: '/setup', desc: 'Reconfigure API key, URL & models' },
|
|
896
1100
|
{ cmd: '/settings', desc: 'View & configure limits' },
|
|
1101
|
+
{ cmd: '/permissions', desc: 'View permission rules & recent denials' },
|
|
897
1102
|
{ cmd: '/shortcuts', desc: 'Keyboard shortcuts' },
|
|
898
1103
|
{ cmd: '/hooks', desc: 'Show lifecycle hooks' },
|
|
899
1104
|
{ cmd: '/commands', desc: 'Show custom commands' },
|
|
1105
|
+
{ cmd: '/skills', desc: 'List available skills' },
|
|
900
1106
|
{ cmd: '/delegate', desc: 'Run task with sub-agent' },
|
|
1107
|
+
{ cmd: '/agents', desc: 'Show active agents & usage' },
|
|
901
1108
|
{ cmd: '/checkpoint', desc: 'Create git checkpoint' },
|
|
902
1109
|
{ cmd: '/undo', desc: 'Undo last checkpoint' },
|
|
903
1110
|
{ cmd: '/checkpoints', desc: 'List recent checkpoints' },
|
|
@@ -905,6 +1112,9 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
905
1112
|
{ cmd: '/review', desc: 'AI code review of current changes' },
|
|
906
1113
|
{ cmd: '/watch', desc: 'Run command, auto-fix on failure' },
|
|
907
1114
|
{ cmd: '/create', desc: 'Scaffold a new project' },
|
|
1115
|
+
{ cmd: '/worktree', desc: 'Manage git worktrees for isolated work' },
|
|
1116
|
+
{ cmd: '/plugin', desc: 'Install/enable/disable/uninstall plugins' },
|
|
1117
|
+
{ cmd: '/notify', desc: 'Test terminal notification' },
|
|
908
1118
|
{ cmd: '/exit', desc: 'Save & exit' },
|
|
909
1119
|
...customCommands.map((c) => ({ cmd: `/${c.name}`, desc: c.description })),
|
|
910
1120
|
], [customCommands]);
|
|
@@ -913,18 +1123,37 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
913
1123
|
const filteredCommands = showCommandPicker
|
|
914
1124
|
? commands.filter((c) => c.cmd.startsWith(prefix))
|
|
915
1125
|
: [];
|
|
916
|
-
|
|
1126
|
+
// @-mention file typeahead
|
|
1127
|
+
const atMatch = value.match(/@(\S*)$/);
|
|
1128
|
+
const atQuery = atMatch?.[1]?.toLowerCase() ?? '';
|
|
1129
|
+
const showFilePicker = !disabled && !!atMatch && projectIndex;
|
|
1130
|
+
const filteredFiles = useMemo(() => {
|
|
1131
|
+
if (!showFilePicker || !projectIndex)
|
|
1132
|
+
return [];
|
|
1133
|
+
const q = atQuery;
|
|
1134
|
+
return projectIndex.files
|
|
1135
|
+
.map(f => f.relativePath)
|
|
1136
|
+
.filter(p => {
|
|
1137
|
+
if (!q)
|
|
1138
|
+
return true;
|
|
1139
|
+
const name = p.split('/').pop()?.toLowerCase() ?? '';
|
|
1140
|
+
return name.includes(q) || p.toLowerCase().includes(q);
|
|
1141
|
+
})
|
|
1142
|
+
.slice(0, 10);
|
|
1143
|
+
}, [showFilePicker, atQuery]);
|
|
1144
|
+
const pickerActive = (showCommandPicker && filteredCommands.length > 0) || (showFilePicker && filteredFiles.length > 0);
|
|
917
1145
|
useEffect(() => {
|
|
918
1146
|
if (!onHeightChange)
|
|
919
1147
|
return;
|
|
920
1148
|
let h = 3; // separator + input row + padding
|
|
1149
|
+
const pickerCount = showCommandPicker ? filteredCommands.length : filteredFiles.length;
|
|
921
1150
|
if (pickerActive)
|
|
922
|
-
h += Math.min(
|
|
1151
|
+
h += Math.min(pickerCount, 10) + 2;
|
|
923
1152
|
if (stash.length > 0 && !disabled)
|
|
924
1153
|
h += 1;
|
|
925
1154
|
onHeightChange(h);
|
|
926
|
-
}, [pickerActive, filteredCommands.length, stash.length, disabled, onHeightChange]);
|
|
927
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingX: 2, children: _jsx(Text, { dimColor: true, children: line }) }),
|
|
1155
|
+
}, [pickerActive, filteredCommands.length, filteredFiles.length, stash.length, disabled, onHeightChange, showCommandPicker]);
|
|
1156
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingX: 2, children: _jsx(Text, { dimColor: true, children: line }) }), showCommandPicker && filteredCommands.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingX: 3, marginX: 0, paddingY: 0, children: [_jsxs(Box, { gap: 1, marginBottom: 0, children: [_jsx(Text, { dimColor: true, bold: true, children: "Commands" }), _jsxs(Text, { dimColor: true, children: ["(", filteredCommands.length, ")"] })] }), _jsx(SelectInput, { items: filteredCommands.map((c) => ({
|
|
928
1157
|
label: `${c.cmd.padEnd(16)} ${c.desc}`,
|
|
929
1158
|
value: c.cmd,
|
|
930
1159
|
})), isFocused: true, onSelect: (item) => {
|
|
@@ -936,7 +1165,14 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
936
1165
|
onChange('');
|
|
937
1166
|
handleSubmit(item.value);
|
|
938
1167
|
}
|
|
939
|
-
}, limit: 10 }), _jsxs(Text, { dimColor: true, italic: true, children: ['↑↓', " navigate ", '•', " Enter select ", '•', " keep typing to filter"] })] })),
|
|
1168
|
+
}, limit: 10 }), _jsxs(Text, { dimColor: true, italic: true, children: ['↑↓', " navigate ", '•', " Enter select ", '•', " keep typing to filter"] })] })), showFilePicker && filteredFiles.length > 0 && !showCommandPicker && (_jsxs(Box, { flexDirection: "column", paddingX: 3, marginX: 0, paddingY: 0, children: [_jsxs(Box, { gap: 1, marginBottom: 0, children: [_jsx(Text, { dimColor: true, bold: true, children: "Files" }), _jsxs(Text, { dimColor: true, children: ["(", filteredFiles.length, ")"] })] }), _jsx(SelectInput, { items: filteredFiles.map((f) => ({
|
|
1169
|
+
label: f,
|
|
1170
|
+
value: f,
|
|
1171
|
+
})), isFocused: true, onSelect: (item) => {
|
|
1172
|
+
// Replace @query with the selected file path
|
|
1173
|
+
const newValue = value.replace(/@\S*$/, '@' + item.value + ' ');
|
|
1174
|
+
onChange(newValue);
|
|
1175
|
+
}, limit: 10 }), _jsxs(Text, { dimColor: true, italic: true, children: ['↑↓', " navigate ", '•', " Enter select ", '•', " keep typing to filter"] })] })), searchMode && (_jsxs(Box, { paddingX: 3, children: [_jsxs(Text, { color: "cyan", dimColor: true, children: ['🔍', " reverse-i-search: "] }), _jsx(Text, { color: "cyan", bold: true, children: searchQuery || '' }), _jsx(Text, { dimColor: true, children: " (Enter to select, Esc to cancel)" })] })), imageChips.length > 0 && !disabled && (_jsxs(Box, { paddingX: 3, gap: 1, children: [imageChips.map((_, i) => (_jsxs(Text, { color: "blue", bold: true, children: ["[Image #", i + 1, "]"] }, i))), _jsxs(Text, { dimColor: true, children: ["(", imageChips.length, " pasted)"] })] })), stash.length > 0 && !disabled && !searchMode && (_jsxs(Box, { paddingX: 3, children: [_jsxs(Text, { color: "magenta", dimColor: true, children: ['⚑', " Stashed: ", truncate(stash, 40), " "] }), _jsx(Text, { dimColor: true, children: "(Ctrl+S to restore)" })] })), _jsx(Box, { paddingX: 2, children: _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { flexShrink: 0, width: 2, children: disabled ? (_jsx(Text, { color: "magentaBright", children: _jsx(Spinner, { type: "dots" }) })) : (_jsx(Text, { color: "magentaBright", bold: true, children: '❯' })) }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: value, onChange: onChange, onSubmit: handleSubmit, placeholder: disabled ? 'Queue a message... (sent after current task)' : 'Type a message...', showCursor: true, focus: !pickerActive }) }), disabled && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, children: " ^C interrupt" }) }))] }) })] }));
|
|
940
1176
|
});
|
|
941
1177
|
// ─── App ───
|
|
942
1178
|
function estimateLines(item, cols) {
|
|
@@ -956,12 +1192,14 @@ function estimateLines(item, cols) {
|
|
|
956
1192
|
return totalLines + markdownExpansion + 2; // marginTop=1 + padding
|
|
957
1193
|
}
|
|
958
1194
|
case 'error': return totalLines + 2;
|
|
959
|
-
case 'tool-call': return 2;
|
|
960
|
-
case 'tool-result': return 1;
|
|
1195
|
+
case 'tool-call': return 2;
|
|
1196
|
+
case 'tool-result': return Math.max(1, totalLines);
|
|
961
1197
|
case 'tool-blocked': return 1;
|
|
962
|
-
case '
|
|
963
|
-
case '
|
|
1198
|
+
case 'tool-group': return item.collapsed ? 2 : (item.children?.length ?? 0) * 2;
|
|
1199
|
+
case 'diff': return lines.length + 2;
|
|
1200
|
+
case 'system': return Math.max(1, totalLines);
|
|
964
1201
|
}
|
|
1202
|
+
return totalLines;
|
|
965
1203
|
}
|
|
966
1204
|
function useScrollableItems(items, availableHeight, cols, isLoading, streamLineCount = 0) {
|
|
967
1205
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
@@ -977,7 +1215,7 @@ function useScrollableItems(items, availableHeight, cols, isLoading, streamLineC
|
|
|
977
1215
|
if (isLoading && autoScroll.current)
|
|
978
1216
|
setScrollOffset(0);
|
|
979
1217
|
}, [isLoading]);
|
|
980
|
-
const scrollUp = useCallback((amount =
|
|
1218
|
+
const scrollUp = useCallback((amount = 5) => {
|
|
981
1219
|
setScrollOffset((prev) => {
|
|
982
1220
|
const maxOffset = Math.max(items.length - 1, 0);
|
|
983
1221
|
const next = Math.min(prev + amount, maxOffset);
|
|
@@ -985,7 +1223,7 @@ function useScrollableItems(items, availableHeight, cols, isLoading, streamLineC
|
|
|
985
1223
|
return next;
|
|
986
1224
|
});
|
|
987
1225
|
}, [items.length]);
|
|
988
|
-
const scrollDown = useCallback((amount =
|
|
1226
|
+
const scrollDown = useCallback((amount = 5) => {
|
|
989
1227
|
setScrollOffset((prev) => {
|
|
990
1228
|
const next = Math.max(prev - amount, 0);
|
|
991
1229
|
if (next === 0)
|
|
@@ -1026,17 +1264,21 @@ function App({ config }) {
|
|
|
1026
1264
|
const [thinkStart, setThinkStart] = useState(null);
|
|
1027
1265
|
const [showBanner, setShowBanner] = useState(true);
|
|
1028
1266
|
const [totalTokens, setTotalTokens] = useState(0);
|
|
1267
|
+
const [contextPercent, setContextPercent] = useState(0);
|
|
1268
|
+
const [rateLimitWarn, setRateLimitWarn] = useState(false);
|
|
1029
1269
|
const [sessionList, setSessionList] = useState(null);
|
|
1030
1270
|
const [showSetup, setShowSetup] = useState(false);
|
|
1031
1271
|
const [currentMode, setCurrentMode] = useState(agent?.getPermissionMode() ?? 'default');
|
|
1032
1272
|
const [selectingMode, setSelectingMode] = useState(false);
|
|
1033
1273
|
const [editingSettings, setEditingSettings] = useState(false);
|
|
1274
|
+
const [agentVersion, setAgentVersion] = useState(0);
|
|
1034
1275
|
const counter = useRef(0);
|
|
1035
1276
|
const showBannerRef = useRef(true);
|
|
1036
1277
|
const modelsRef = useRef(models);
|
|
1037
1278
|
useEffect(() => { modelsRef.current = models; }, [models]);
|
|
1038
1279
|
const nextId = () => ++counter.current;
|
|
1039
1280
|
const MAX_DISPLAY_ITEMS = 2000;
|
|
1281
|
+
const TOOL_GROUP_THRESHOLD = 3;
|
|
1040
1282
|
const add = useCallback((kind, text, meta, hint) => {
|
|
1041
1283
|
setItems((prev) => {
|
|
1042
1284
|
const next = [...prev, { id: nextId(), kind, text, meta, hint }];
|
|
@@ -1047,6 +1289,29 @@ function App({ config }) {
|
|
|
1047
1289
|
setShowBanner(false);
|
|
1048
1290
|
}
|
|
1049
1291
|
}, []);
|
|
1292
|
+
const collapseToolGroups = useCallback(() => {
|
|
1293
|
+
setItems(prev => {
|
|
1294
|
+
const isToolItem = (k) => k === 'tool-call' || k === 'tool-result';
|
|
1295
|
+
let i = prev.length - 1;
|
|
1296
|
+
while (i >= 0 && isToolItem(prev[i].kind))
|
|
1297
|
+
i--;
|
|
1298
|
+
const toolStart = i + 1;
|
|
1299
|
+
const toolItems = prev.slice(toolStart);
|
|
1300
|
+
if (toolItems.length < TOOL_GROUP_THRESHOLD)
|
|
1301
|
+
return prev;
|
|
1302
|
+
const group = {
|
|
1303
|
+
id: nextId(),
|
|
1304
|
+
kind: 'tool-group',
|
|
1305
|
+
text: `Ran ${Math.ceil(toolItems.length / 2)} tools`,
|
|
1306
|
+
children: toolItems,
|
|
1307
|
+
collapsed: true,
|
|
1308
|
+
};
|
|
1309
|
+
return [...prev.slice(0, toolStart), group];
|
|
1310
|
+
});
|
|
1311
|
+
}, []);
|
|
1312
|
+
const toggleToolGroups = useCallback(() => {
|
|
1313
|
+
setItems(prev => prev.map(item => item.kind === 'tool-group' ? { ...item, collapsed: !item.collapsed } : item));
|
|
1314
|
+
}, []);
|
|
1050
1315
|
// Register agent event handlers ONCE
|
|
1051
1316
|
useEffect(() => {
|
|
1052
1317
|
const onThinkingStart = () => {
|
|
@@ -1098,9 +1363,18 @@ function App({ config }) {
|
|
|
1098
1363
|
streamBuffer = '';
|
|
1099
1364
|
setStreamText('');
|
|
1100
1365
|
setReasoningText('');
|
|
1366
|
+
collapseToolGroups();
|
|
1101
1367
|
if (full.trim()) {
|
|
1102
1368
|
add('assistant', full);
|
|
1103
1369
|
}
|
|
1370
|
+
// Auto-save session after each assistant turn
|
|
1371
|
+
if (agent) {
|
|
1372
|
+
const msgs = agent.getMessages();
|
|
1373
|
+
if (msgs.length > 0) {
|
|
1374
|
+
saveCrashState(sessionId, msgs, modelsRef.current[0] || 'unknown', costTracker.getState())
|
|
1375
|
+
.catch(() => { });
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1104
1378
|
};
|
|
1105
1379
|
let reasoningBuffer = '';
|
|
1106
1380
|
let reasoningFlushTimer = null;
|
|
@@ -1170,6 +1444,8 @@ function App({ config }) {
|
|
|
1170
1444
|
const lower = reason?.toLowerCase() || '';
|
|
1171
1445
|
if (lower.includes('rate limit') || lower.includes('429')) {
|
|
1172
1446
|
hint = 'Rate limited. Try /model to switch models, or wait a moment.';
|
|
1447
|
+
setRateLimitWarn(true);
|
|
1448
|
+
setTimeout(() => setRateLimitWarn(false), 120_000);
|
|
1173
1449
|
}
|
|
1174
1450
|
else if (lower.includes('auth') || lower.includes('401') || lower.includes('api key')) {
|
|
1175
1451
|
hint = 'Authentication failed. Run /setup to reconfigure your API key.';
|
|
@@ -1189,6 +1465,7 @@ function App({ config }) {
|
|
|
1189
1465
|
add('system', `Context compressed: ${formatTokens(info.saved)} tokens saved (${formatTokens(info.before)} \u2192 ${formatTokens(info.after)})`);
|
|
1190
1466
|
};
|
|
1191
1467
|
const onContextUsage = (info) => {
|
|
1468
|
+
setContextPercent(info.percentage);
|
|
1192
1469
|
if (info.percentage > 80) {
|
|
1193
1470
|
add('system', `Context usage: ${info.percentage}% (${formatTokens(info.tokens)}/${formatTokens(info.maxTokens)} tokens)`);
|
|
1194
1471
|
}
|
|
@@ -1235,7 +1512,41 @@ function App({ config }) {
|
|
|
1235
1512
|
currentAgent.off('context:compress', onContextCompress);
|
|
1236
1513
|
currentAgent.off('context:usage', onContextUsage);
|
|
1237
1514
|
};
|
|
1515
|
+
}, [add, agentVersion]);
|
|
1516
|
+
// Crash recovery: check for recoverable session on startup
|
|
1517
|
+
useEffect(() => {
|
|
1518
|
+
loadCrashState().then(state => {
|
|
1519
|
+
if (state && state.messages.length > 0 && agent) {
|
|
1520
|
+
add('system', `Recoverable session found (${state.messages.length} messages). Use /resume to restore.`);
|
|
1521
|
+
}
|
|
1522
|
+
}).catch(() => { });
|
|
1238
1523
|
}, [add]);
|
|
1524
|
+
// Idle session nudge: suggest /clear after 75+ minutes of inactivity
|
|
1525
|
+
const lastActivityRef = useRef(Date.now());
|
|
1526
|
+
const idleNudgedRef = useRef(false);
|
|
1527
|
+
useEffect(() => {
|
|
1528
|
+
const IDLE_THRESHOLD_MS = 75 * 60_000; // 75 minutes
|
|
1529
|
+
const timer = setInterval(() => {
|
|
1530
|
+
const idle = Date.now() - lastActivityRef.current;
|
|
1531
|
+
if (idle >= IDLE_THRESHOLD_MS && !idleNudgedRef.current && agent && agent.getMessages().length > 10) {
|
|
1532
|
+
idleNudgedRef.current = true;
|
|
1533
|
+
add('system', 'Session idle for 75+ minutes. Consider /clear to optimize token usage, or /compact to compress context.');
|
|
1534
|
+
}
|
|
1535
|
+
}, 60_000);
|
|
1536
|
+
return () => clearInterval(timer);
|
|
1537
|
+
}, [add]);
|
|
1538
|
+
// Terminal notification on task completion
|
|
1539
|
+
useEffect(() => {
|
|
1540
|
+
if (!agent)
|
|
1541
|
+
return;
|
|
1542
|
+
const onStreamEnd = () => {
|
|
1543
|
+
if (!document.hasFocus?.() || process.env.SCALIX_NOTIFY !== '0') {
|
|
1544
|
+
sendNotification('Scalix', 'Task complete');
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
agent.on('stream:end', onStreamEnd);
|
|
1548
|
+
return () => { agent.off('stream:end', onStreamEnd); };
|
|
1549
|
+
}, [agentVersion]);
|
|
1239
1550
|
// Confirmation bridge (event-driven)
|
|
1240
1551
|
useEffect(() => {
|
|
1241
1552
|
const onPending = () => {
|
|
@@ -1246,8 +1557,24 @@ function App({ config }) {
|
|
|
1246
1557
|
confirmEmitter.on('pending', onPending);
|
|
1247
1558
|
return () => { confirmEmitter.off('pending', onPending); };
|
|
1248
1559
|
}, [confirmInfo]);
|
|
1249
|
-
|
|
1560
|
+
useEffect(() => {
|
|
1561
|
+
const banner = getUpdateBanner();
|
|
1562
|
+
if (banner)
|
|
1563
|
+
add('system', banner);
|
|
1564
|
+
}, [add]);
|
|
1565
|
+
const handleConfirm = useCallback((ok, always) => {
|
|
1250
1566
|
if (pendingConfirm) {
|
|
1567
|
+
if (ok && always)
|
|
1568
|
+
alwaysAllowedTools.add(pendingConfirm.toolName);
|
|
1569
|
+
if (!ok) {
|
|
1570
|
+
recentDenials.push({
|
|
1571
|
+
toolName: pendingConfirm.toolName,
|
|
1572
|
+
args: JSON.stringify(pendingConfirm.args).slice(0, 100),
|
|
1573
|
+
timestamp: Date.now(),
|
|
1574
|
+
});
|
|
1575
|
+
if (recentDenials.length > 20)
|
|
1576
|
+
recentDenials.shift();
|
|
1577
|
+
}
|
|
1251
1578
|
pendingConfirm.resolve(ok);
|
|
1252
1579
|
pendingConfirm = null;
|
|
1253
1580
|
}
|
|
@@ -1285,6 +1612,7 @@ function App({ config }) {
|
|
|
1285
1612
|
' /checkpoint \u2014 create git checkpoint',
|
|
1286
1613
|
' /undo \u2014 undo last checkpoint',
|
|
1287
1614
|
' /checkpoints \u2014 list recent checkpoints',
|
|
1615
|
+
' /worktree \u2014 manage git worktrees (list/create/remove/cleanup)',
|
|
1288
1616
|
' /git \u2014 show git context',
|
|
1289
1617
|
'',
|
|
1290
1618
|
'Project:',
|
|
@@ -1295,15 +1623,19 @@ function App({ config }) {
|
|
|
1295
1623
|
' /watch <cmd> \u2014 run command, auto-fix on failure',
|
|
1296
1624
|
' /create <d> \u2014 scaffold a new project',
|
|
1297
1625
|
' /delegate <t> \u2014 run task with sub-agent',
|
|
1626
|
+
' /agents \u2014 show active agents & usage',
|
|
1298
1627
|
'',
|
|
1299
1628
|
'Tools & Config:',
|
|
1300
1629
|
' /copy \u2014 copy last response to clipboard',
|
|
1301
1630
|
' /status \u2014 show version, model & connection info',
|
|
1631
|
+
' /update \u2014 check for and install updates',
|
|
1302
1632
|
' /doctor \u2014 diagnose installation',
|
|
1303
1633
|
' /settings \u2014 view & configure limits',
|
|
1304
1634
|
' /setup \u2014 reconfigure API key, URL & models',
|
|
1305
1635
|
' /mcp <l> <u> \u2014 add MCP server',
|
|
1306
1636
|
' /plugins \u2014 show loaded plugins',
|
|
1637
|
+
' /plugin \u2014 install/enable/disable/uninstall plugins',
|
|
1638
|
+
' /notify \u2014 test terminal notification',
|
|
1307
1639
|
' /router \u2014 show model routing status',
|
|
1308
1640
|
' /replay \u2014 show session timeline',
|
|
1309
1641
|
' /hooks \u2014 show lifecycle hooks',
|
|
@@ -1331,7 +1663,7 @@ function App({ config }) {
|
|
|
1331
1663
|
if (msgs.length > 0) {
|
|
1332
1664
|
saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).then((meta) => {
|
|
1333
1665
|
add('system', `Auto-saved: ${meta.name}`);
|
|
1334
|
-
}).catch(() => { });
|
|
1666
|
+
}).catch((err) => { logger.warn('Failed to auto-save session', { error: err.message }); });
|
|
1335
1667
|
}
|
|
1336
1668
|
agent.clearHistory();
|
|
1337
1669
|
setItems([]);
|
|
@@ -1345,6 +1677,8 @@ function App({ config }) {
|
|
|
1345
1677
|
setShowBanner(true);
|
|
1346
1678
|
costTracker.reset();
|
|
1347
1679
|
setTotalTokens(0);
|
|
1680
|
+
setContextPercent(0);
|
|
1681
|
+
setRateLimitWarn(false);
|
|
1348
1682
|
sessionId = generateSessionId();
|
|
1349
1683
|
return true;
|
|
1350
1684
|
}
|
|
@@ -1397,7 +1731,35 @@ function App({ config }) {
|
|
|
1397
1731
|
});
|
|
1398
1732
|
return true;
|
|
1399
1733
|
}
|
|
1400
|
-
if (cmd === '/load') {
|
|
1734
|
+
if (cmd === '/load' || cmd === '/resume') {
|
|
1735
|
+
if (cmd === '/resume') {
|
|
1736
|
+
loadCrashState().then(async (state) => {
|
|
1737
|
+
if (state && state.messages.length > 0 && agent) {
|
|
1738
|
+
agent.loadMessages(state.messages);
|
|
1739
|
+
sessionId = state.sessionId;
|
|
1740
|
+
if (state.costState) {
|
|
1741
|
+
costTracker.restore(state.costState);
|
|
1742
|
+
setTotalTokens(state.costState.totalTokens);
|
|
1743
|
+
}
|
|
1744
|
+
add('system', `Resumed session with ${state.messages.length} messages.`);
|
|
1745
|
+
showBannerRef.current = false;
|
|
1746
|
+
setShowBanner(false);
|
|
1747
|
+
await clearCrashState();
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
const sessions = await listSessions();
|
|
1751
|
+
if (sessions.length === 0) {
|
|
1752
|
+
add('system', 'No saved sessions or crash recovery data found.');
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
setSessionList(sessions);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}).catch(() => {
|
|
1759
|
+
add('system', 'Failed to load crash recovery data.');
|
|
1760
|
+
});
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1401
1763
|
listSessions().then((sessions) => {
|
|
1402
1764
|
if (sessions.length === 0) {
|
|
1403
1765
|
add('system', 'No saved sessions found.');
|
|
@@ -1408,20 +1770,43 @@ function App({ config }) {
|
|
|
1408
1770
|
});
|
|
1409
1771
|
return true;
|
|
1410
1772
|
}
|
|
1773
|
+
if (cmd === '/fork') {
|
|
1774
|
+
const pointStr = text.split(' ')[1];
|
|
1775
|
+
const point = pointStr ? parseInt(pointStr, 10) : agent?.getMessages().length;
|
|
1776
|
+
if (!point || point <= 0) {
|
|
1777
|
+
add('system', 'Usage: /fork [message_number] — fork conversation from a specific point');
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
forkSession(sessionId, point).then(({ id, meta }) => {
|
|
1781
|
+
add('system', `Forked to new session: ${meta.name} (${id.slice(0, 8)})`);
|
|
1782
|
+
agent.loadMessages(agent.getMessages().slice(0, point));
|
|
1783
|
+
sessionId = id;
|
|
1784
|
+
}).catch((err) => {
|
|
1785
|
+
add('error', `Fork failed: ${err.message}`);
|
|
1786
|
+
});
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1411
1789
|
if (cmd === '/shortcuts') {
|
|
1412
1790
|
const shortcuts = [
|
|
1413
1791
|
'Ctrl+C \u2014 interrupt agent / clear input / double-press to quit',
|
|
1414
1792
|
'Ctrl+D \u2014 exit (when input is empty)',
|
|
1415
1793
|
'Ctrl+L \u2014 clear screen & conversation',
|
|
1794
|
+
'Ctrl+O \u2014 toggle collapsed tool groups',
|
|
1416
1795
|
'Ctrl+U \u2014 clear entire input line',
|
|
1417
1796
|
'Ctrl+W \u2014 delete last word',
|
|
1418
1797
|
'Ctrl+K \u2014 delete to end of line',
|
|
1419
1798
|
'Ctrl+S \u2014 stash / restore current prompt',
|
|
1799
|
+
'Ctrl+V \u2014 paste image from clipboard',
|
|
1800
|
+
'Ctrl+R \u2014 reverse history search',
|
|
1801
|
+
'Ctrl+X Ctrl+E \u2014 open in external editor ($EDITOR)',
|
|
1802
|
+
'Alt+Enter \u2014 insert newline (multiline)',
|
|
1420
1803
|
'Tab \u2014 autocomplete slash commands',
|
|
1421
1804
|
'\u2191/\u2193 \u2014 navigate input history',
|
|
1422
|
-
'Shift+\u2191
|
|
1423
|
-
'Shift+\u2193
|
|
1424
|
-
'
|
|
1805
|
+
'Shift+\u2191 \u2014 scroll up (5 items)',
|
|
1806
|
+
'Shift+\u2193 \u2014 scroll down (5 items)',
|
|
1807
|
+
'PageUp \u2014 scroll up fast (15 items)',
|
|
1808
|
+
'PageDown \u2014 scroll down fast (15 items)',
|
|
1809
|
+
'Y/A/N \u2014 allow / always allow / deny tool execution',
|
|
1425
1810
|
'Esc \u2014 quit',
|
|
1426
1811
|
'',
|
|
1427
1812
|
'Copy \u2014 Cmd+C (Mac) / Ctrl+Shift+C (Linux)',
|
|
@@ -1433,6 +1818,32 @@ function App({ config }) {
|
|
|
1433
1818
|
shortcuts.forEach((s) => add('system', s));
|
|
1434
1819
|
return true;
|
|
1435
1820
|
}
|
|
1821
|
+
if (cmd === '/permissions') {
|
|
1822
|
+
add('system', `Permission Mode: ${currentMode.toUpperCase()} — ${getModeDescription(currentMode)}`);
|
|
1823
|
+
if (alwaysAllowedTools.size > 0) {
|
|
1824
|
+
add('system', `Always Allowed (session): ${[...alwaysAllowedTools].join(', ')}`);
|
|
1825
|
+
}
|
|
1826
|
+
const scopedRules = getScopedPermissionRules();
|
|
1827
|
+
if (scopedRules.length > 0) {
|
|
1828
|
+
add('system', 'Scoped Rules:');
|
|
1829
|
+
scopedRules.forEach(r => {
|
|
1830
|
+
const spec = r.prefix ? `${r.tool}(${r.prefix}*)` : r.tool;
|
|
1831
|
+
add('system', ` ${r.action === 'allow' ? '✓' : '✗'} ${spec}${r.regex ? ` [regex: ${r.regex}]` : ''}`);
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
if (recentDenials.length > 0) {
|
|
1835
|
+
add('system', 'Recent Denials:');
|
|
1836
|
+
const recent = recentDenials.slice(-10);
|
|
1837
|
+
recent.forEach(d => {
|
|
1838
|
+
const ago = Math.round((Date.now() - d.timestamp) / 1000);
|
|
1839
|
+
add('system', ` ✗ ${d.toolName} (${ago}s ago) ${d.args}`);
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
add('system', 'No recent denials.');
|
|
1844
|
+
}
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1436
1847
|
if (cmd === '/settings') {
|
|
1437
1848
|
setEditingSettings(true);
|
|
1438
1849
|
return true;
|
|
@@ -1449,7 +1860,7 @@ function App({ config }) {
|
|
|
1449
1860
|
loadConfig().then(cfg => {
|
|
1450
1861
|
cfg.permissionMode = arg;
|
|
1451
1862
|
saveConfig(cfg);
|
|
1452
|
-
}).catch(() => { });
|
|
1863
|
+
}).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
|
|
1453
1864
|
add('system', `Mode: ${arg.toUpperCase()} \u2014 ${getModeDescription(arg)}`);
|
|
1454
1865
|
}
|
|
1455
1866
|
else {
|
|
@@ -1464,7 +1875,7 @@ function App({ config }) {
|
|
|
1464
1875
|
loadConfig().then(cfg => {
|
|
1465
1876
|
cfg.thinkingMode = arg;
|
|
1466
1877
|
saveConfig(cfg);
|
|
1467
|
-
}).catch(() => { });
|
|
1878
|
+
}).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
|
|
1468
1879
|
add('system', `Thinking: ${arg.toUpperCase()}${arg === 'off' ? '' : ' \u2014 model will reason before responding'}`);
|
|
1469
1880
|
}
|
|
1470
1881
|
else {
|
|
@@ -1510,14 +1921,15 @@ function App({ config }) {
|
|
|
1510
1921
|
return true;
|
|
1511
1922
|
}
|
|
1512
1923
|
if (cmd === '/git') {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1924
|
+
collectGitInfo(process.cwd()).then((gitInfo) => {
|
|
1925
|
+
if (!gitInfo.isRepo) {
|
|
1926
|
+
add('system', 'Not a git repository.');
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
const lines = gitInfoToContext(gitInfo).split('\n');
|
|
1930
|
+
lines.forEach((l) => add('system', l));
|
|
1931
|
+
}
|
|
1932
|
+
}).catch(() => { add('system', 'Failed to collect git info.'); });
|
|
1521
1933
|
return true;
|
|
1522
1934
|
}
|
|
1523
1935
|
if (cmd === '/plugins') {
|
|
@@ -1574,6 +1986,10 @@ function App({ config }) {
|
|
|
1574
1986
|
commandsSummary(customCommands).split('\n').forEach((l) => add('system', l));
|
|
1575
1987
|
return true;
|
|
1576
1988
|
}
|
|
1989
|
+
if (cmd === '/skills') {
|
|
1990
|
+
skillsSummary(loadedSkills).split('\n').forEach((l) => add('system', l));
|
|
1991
|
+
return true;
|
|
1992
|
+
}
|
|
1577
1993
|
if (cmd === '/delegate') {
|
|
1578
1994
|
const prompt = text.slice(10).trim();
|
|
1579
1995
|
if (!prompt) {
|
|
@@ -1597,6 +2013,23 @@ function App({ config }) {
|
|
|
1597
2013
|
});
|
|
1598
2014
|
return true;
|
|
1599
2015
|
}
|
|
2016
|
+
if (cmd === '/agents') {
|
|
2017
|
+
const registry = AgentRegistry.getInstance();
|
|
2018
|
+
const summary = registry.getUsageSummary();
|
|
2019
|
+
const tree = registry.getAgentTree();
|
|
2020
|
+
const lines = [
|
|
2021
|
+
`Active: ${summary.activeCount} | Completed: ${summary.completedCount} | Failed: ${summary.failedCount}`,
|
|
2022
|
+
`Total tokens: ${summary.totalTokens.toLocaleString()}`,
|
|
2023
|
+
];
|
|
2024
|
+
if (tree && tree !== '(no agents)') {
|
|
2025
|
+
lines.push('', 'Agent tree:', tree);
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
lines.push('', 'No agents have been spawned yet.');
|
|
2029
|
+
}
|
|
2030
|
+
lines.forEach(l => add('system', l));
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
1600
2033
|
if (cmd === '/checkpoint') {
|
|
1601
2034
|
const label = text.slice(12).trim() || undefined;
|
|
1602
2035
|
createCheckpoint(process.cwd(), label).then((hash) => {
|
|
@@ -1624,6 +2057,130 @@ function App({ config }) {
|
|
|
1624
2057
|
}).catch(() => add('error', 'Failed to list checkpoints.'));
|
|
1625
2058
|
return true;
|
|
1626
2059
|
}
|
|
2060
|
+
if (cmd === '/worktree') {
|
|
2061
|
+
const subArgs = text.slice(10).trim().split(/\s+/);
|
|
2062
|
+
const subCmd = subArgs[0] || 'list';
|
|
2063
|
+
const cwd = process.cwd();
|
|
2064
|
+
if (subCmd === 'list' || subCmd === 'ls') {
|
|
2065
|
+
listWorktrees(cwd).then(wts => {
|
|
2066
|
+
if (wts.length === 0)
|
|
2067
|
+
add('system', 'No active worktrees.');
|
|
2068
|
+
else {
|
|
2069
|
+
add('system', `${wts.length} worktree(s):`);
|
|
2070
|
+
wts.forEach(wt => add('system', ` ${wt.branch} → ${wt.path}`));
|
|
2071
|
+
}
|
|
2072
|
+
}).catch(err => add('error', `Failed to list worktrees: ${err.message}`));
|
|
2073
|
+
}
|
|
2074
|
+
else if (subCmd === 'create' || subCmd === 'new') {
|
|
2075
|
+
const name = subArgs[1] || `work-${Date.now().toString(36)}`;
|
|
2076
|
+
createWorktree(name, cwd).then(wt => {
|
|
2077
|
+
add('system', `Worktree created: ${wt.branch} → ${wt.path}`);
|
|
2078
|
+
add('system', `cd ${wt.path} to work in isolation`);
|
|
2079
|
+
}).catch(err => add('error', `Failed to create worktree: ${err.message}`));
|
|
2080
|
+
}
|
|
2081
|
+
else if (subCmd === 'remove' || subCmd === 'rm') {
|
|
2082
|
+
const branchOrId = subArgs[1];
|
|
2083
|
+
if (!branchOrId) {
|
|
2084
|
+
add('system', 'Usage: /worktree remove <branch-or-agent-id>');
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
listWorktrees(cwd).then(wts => {
|
|
2088
|
+
const wt = wts.find(w => w.branch.includes(branchOrId) || w.agentId.includes(branchOrId));
|
|
2089
|
+
if (!wt) {
|
|
2090
|
+
add('error', `Worktree not found: ${branchOrId}`);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
removeWorktree(wt, cwd).then(() => {
|
|
2094
|
+
add('system', `Worktree removed: ${wt.branch}`);
|
|
2095
|
+
}).catch(err => add('error', `Failed to remove: ${err.message}`));
|
|
2096
|
+
}).catch(err => add('error', `Failed: ${err.message}`));
|
|
2097
|
+
}
|
|
2098
|
+
else if (subCmd === 'cleanup') {
|
|
2099
|
+
cleanupStaleWorktrees(cwd).then(count => {
|
|
2100
|
+
add('system', count > 0 ? `Cleaned up ${count} stale worktree(s).` : 'No stale worktrees found.');
|
|
2101
|
+
}).catch(err => add('error', `Cleanup failed: ${err.message}`));
|
|
2102
|
+
}
|
|
2103
|
+
else {
|
|
2104
|
+
add('system', 'Usage: /worktree [list|create <name>|remove <branch>|cleanup]');
|
|
2105
|
+
}
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
2108
|
+
if (cmd === '/plugin') {
|
|
2109
|
+
const subArgs = text.slice(8).trim().split(/\s+/);
|
|
2110
|
+
const subCmd = subArgs[0] || 'list';
|
|
2111
|
+
const cwd = process.cwd();
|
|
2112
|
+
if (subCmd === 'list' || subCmd === 'ls') {
|
|
2113
|
+
listInstalledPlugins(cwd).then(plugins => {
|
|
2114
|
+
if (plugins.length === 0) {
|
|
2115
|
+
add('system', 'No plugins installed. Use /plugin install <source> to add one.');
|
|
2116
|
+
}
|
|
2117
|
+
else {
|
|
2118
|
+
add('system', `${plugins.length} plugin(s) installed:`);
|
|
2119
|
+
plugins.forEach(p => {
|
|
2120
|
+
const status = p.enabled ? 'enabled' : 'disabled';
|
|
2121
|
+
add('system', ` ${p.name} v${p.version} [${status}]`);
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
}).catch(err => add('error', `Failed to list plugins: ${err.message}`));
|
|
2125
|
+
}
|
|
2126
|
+
else if (subCmd === 'install') {
|
|
2127
|
+
const source = subArgs[1];
|
|
2128
|
+
if (!source) {
|
|
2129
|
+
add('system', 'Usage: /plugin install <path-or-git-url>');
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
add('system', `Installing plugin from ${source}...`);
|
|
2133
|
+
installPlugin(cwd, source).then(result => {
|
|
2134
|
+
if (result) {
|
|
2135
|
+
add('system', `Plugin installed: ${result.name} v${result.version}`);
|
|
2136
|
+
add('system', 'Restart session to load new plugin tools.');
|
|
2137
|
+
}
|
|
2138
|
+
else {
|
|
2139
|
+
add('system', 'Plugin already installed.');
|
|
2140
|
+
}
|
|
2141
|
+
}).catch(err => add('error', `Install failed: ${err.message}`));
|
|
2142
|
+
}
|
|
2143
|
+
else if (subCmd === 'uninstall' || subCmd === 'remove') {
|
|
2144
|
+
const name = subArgs[1];
|
|
2145
|
+
if (!name) {
|
|
2146
|
+
add('system', 'Usage: /plugin uninstall <name>');
|
|
2147
|
+
return true;
|
|
2148
|
+
}
|
|
2149
|
+
uninstallPlugin(cwd, name).then(ok => {
|
|
2150
|
+
add(ok ? 'system' : 'error', ok ? `Plugin uninstalled: ${name}` : `Plugin not found: ${name}`);
|
|
2151
|
+
}).catch(err => add('error', `Uninstall failed: ${err.message}`));
|
|
2152
|
+
}
|
|
2153
|
+
else if (subCmd === 'enable') {
|
|
2154
|
+
const name = subArgs[1];
|
|
2155
|
+
if (!name) {
|
|
2156
|
+
add('system', 'Usage: /plugin enable <name>');
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
enablePlugin(cwd, name).then(ok => {
|
|
2160
|
+
add(ok ? 'system' : 'error', ok ? `Plugin enabled: ${name}` : `Plugin not found or already enabled: ${name}`);
|
|
2161
|
+
}).catch(err => add('error', `Enable failed: ${err.message}`));
|
|
2162
|
+
}
|
|
2163
|
+
else if (subCmd === 'disable') {
|
|
2164
|
+
const name = subArgs[1];
|
|
2165
|
+
if (!name) {
|
|
2166
|
+
add('system', 'Usage: /plugin disable <name>');
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
disablePlugin(cwd, name).then(ok => {
|
|
2170
|
+
add(ok ? 'system' : 'error', ok ? `Plugin disabled: ${name}` : `Plugin not found: ${name}`);
|
|
2171
|
+
}).catch(err => add('error', `Disable failed: ${err.message}`));
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
add('system', 'Usage: /plugin [list|install <source>|uninstall <name>|enable <name>|disable <name>]');
|
|
2175
|
+
}
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
if (cmd === '/notify') {
|
|
2179
|
+
const message = text.slice(8).trim() || 'Test notification from Scalix';
|
|
2180
|
+
sendNotification('Scalix', message);
|
|
2181
|
+
add('system', `Notification sent (terminal: ${getTerminalType()})`);
|
|
2182
|
+
return true;
|
|
2183
|
+
}
|
|
1627
2184
|
if (cmd === '/commit') {
|
|
1628
2185
|
setIsLoading(true);
|
|
1629
2186
|
setStatus('thinking');
|
|
@@ -1662,7 +2219,8 @@ function App({ config }) {
|
|
|
1662
2219
|
}
|
|
1663
2220
|
add('system', `Commit message: ${cleanMsg}`);
|
|
1664
2221
|
try {
|
|
1665
|
-
const
|
|
2222
|
+
const fullMsg = cleanMsg + `\n\nCo-Authored-By: ${GIT_BOT_IDENTITY.name} <${GIT_BOT_IDENTITY.email}>`;
|
|
2223
|
+
const result = execFileSync('git', ['commit', '-m', fullMsg], { encoding: 'utf-8', timeout: TOOL_DEFAULTS.gitTimeoutMs, cwd: process.cwd() }).trim();
|
|
1666
2224
|
add('system', result);
|
|
1667
2225
|
}
|
|
1668
2226
|
catch (e) {
|
|
@@ -1871,6 +2429,32 @@ function App({ config }) {
|
|
|
1871
2429
|
add('system', `Messages: ${msgCount}`);
|
|
1872
2430
|
add('system', `Session: ${sessionId}`);
|
|
1873
2431
|
add('system', `CWD: ${process.cwd()}`);
|
|
2432
|
+
const updateBanner = getUpdateBanner();
|
|
2433
|
+
if (updateBanner)
|
|
2434
|
+
add('system', updateBanner);
|
|
2435
|
+
return true;
|
|
2436
|
+
}
|
|
2437
|
+
if (cmd === '/update') {
|
|
2438
|
+
add('system', 'Checking for updates...');
|
|
2439
|
+
checkForUpdate().then((info) => {
|
|
2440
|
+
if (info && info.updateAvailable) {
|
|
2441
|
+
add('system', `New version available: ${info.current} → ${info.latest}`);
|
|
2442
|
+
add('system', 'Installing update...');
|
|
2443
|
+
performUpdate().then(({ success, output }) => {
|
|
2444
|
+
if (success) {
|
|
2445
|
+
add('system', `Updated successfully. Restart Scalix to use v${info.latest}.`);
|
|
2446
|
+
}
|
|
2447
|
+
else {
|
|
2448
|
+
add('system', `Update failed: ${output}\nTry manually: npm install -g scalix@latest`);
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
else {
|
|
2453
|
+
add('system', `Scalix v${VERSION} is up to date.`);
|
|
2454
|
+
}
|
|
2455
|
+
}).catch(() => {
|
|
2456
|
+
add('system', 'Failed to check for updates. Try: npm install -g scalix@latest');
|
|
2457
|
+
});
|
|
1874
2458
|
return true;
|
|
1875
2459
|
}
|
|
1876
2460
|
if (cmd === '/context') {
|
|
@@ -1919,7 +2503,7 @@ function App({ config }) {
|
|
|
1919
2503
|
if (cmd === '/exit' || cmd === '/quit') {
|
|
1920
2504
|
const msgs = agent.getMessages();
|
|
1921
2505
|
if (msgs.length > 0) {
|
|
1922
|
-
saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch(() => { }).finally(() => process.exit(0));
|
|
2506
|
+
saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch((err) => { logger.warn('Failed to save session on exit', { error: err.message }); }).finally(() => process.exit(0));
|
|
1923
2507
|
}
|
|
1924
2508
|
else {
|
|
1925
2509
|
process.exit(0);
|
|
@@ -1932,7 +2516,7 @@ function App({ config }) {
|
|
|
1932
2516
|
loadConfig().then(cfg => {
|
|
1933
2517
|
cfg.permissionMode = 'plan';
|
|
1934
2518
|
saveConfig(cfg);
|
|
1935
|
-
}).catch(() => { });
|
|
2519
|
+
}).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
|
|
1936
2520
|
add('system', `Mode: PLAN — ${getModeDescription('plan')}`);
|
|
1937
2521
|
return true;
|
|
1938
2522
|
}
|
|
@@ -2016,10 +2600,26 @@ function App({ config }) {
|
|
|
2016
2600
|
const isLoadingRef = useRef(false);
|
|
2017
2601
|
useEffect(() => { isLoadingRef.current = isLoading; }, [isLoading]);
|
|
2018
2602
|
const multilineRef = useRef([]);
|
|
2603
|
+
const messageQueueRef = useRef([]);
|
|
2604
|
+
const pendingImagesRef = useRef([]);
|
|
2019
2605
|
const handleSubmit = useCallback(async (text) => {
|
|
2020
2606
|
const trimmed = text.trim();
|
|
2021
|
-
if (!trimmed
|
|
2607
|
+
if (!trimmed)
|
|
2608
|
+
return;
|
|
2609
|
+
// Reset idle nudge on any user activity
|
|
2610
|
+
lastActivityRef.current = Date.now();
|
|
2611
|
+
idleNudgedRef.current = false;
|
|
2612
|
+
// Allow /mode even while the agent is working
|
|
2613
|
+
if (isLoadingRef.current && trimmed.startsWith('/mode')) {
|
|
2614
|
+
handleCommand(trimmed);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
// Queue messages while agent is working
|
|
2618
|
+
if (isLoadingRef.current) {
|
|
2619
|
+
messageQueueRef.current.push(trimmed);
|
|
2620
|
+
add('system', `Queued (${messageQueueRef.current.length}): ${truncate(trimmed, 60)}`);
|
|
2022
2621
|
return;
|
|
2622
|
+
}
|
|
2023
2623
|
if (trimmed.endsWith('\\')) {
|
|
2024
2624
|
if (multilineRef.current.length >= 50) {
|
|
2025
2625
|
add('system', 'Multiline limit reached (50 lines). Sending now.');
|
|
@@ -2041,6 +2641,11 @@ function App({ config }) {
|
|
|
2041
2641
|
}
|
|
2042
2642
|
if (finalText.startsWith('/') && handleCommand(finalText))
|
|
2043
2643
|
return;
|
|
2644
|
+
const triggeredSkill = getSkillByTrigger(loadedSkills, finalText);
|
|
2645
|
+
if (triggeredSkill) {
|
|
2646
|
+
finalText = `${getSkillContent(triggeredSkill, 'full')}\n\n---\nUser request: ${finalText}`;
|
|
2647
|
+
add('system', `Skill activated: ${triggeredSkill.name}`);
|
|
2648
|
+
}
|
|
2044
2649
|
if (finalText.startsWith('!')) {
|
|
2045
2650
|
const shellCmd = finalText.slice(1).trim();
|
|
2046
2651
|
if (!shellCmd)
|
|
@@ -2062,14 +2667,21 @@ function App({ config }) {
|
|
|
2062
2667
|
}
|
|
2063
2668
|
const correction = detectCorrection(finalText);
|
|
2064
2669
|
if (correction) {
|
|
2065
|
-
addNote(process.cwd(), `[correction] ${correction}`).catch(() => { });
|
|
2670
|
+
addNote(process.cwd(), `[correction] ${correction}`).catch((err) => { logger.warn('Failed to save correction note', { error: err.message }); });
|
|
2671
|
+
}
|
|
2672
|
+
const images = pendingImagesRef.current.length > 0 ? [...pendingImagesRef.current] : undefined;
|
|
2673
|
+
if (images) {
|
|
2674
|
+
add('user', `${finalText} [${images.length} image${images.length > 1 ? 's' : ''} attached]`);
|
|
2066
2675
|
}
|
|
2067
|
-
|
|
2676
|
+
else {
|
|
2677
|
+
add('user', finalText);
|
|
2678
|
+
}
|
|
2679
|
+
pendingImagesRef.current = [];
|
|
2068
2680
|
setIsLoading(true);
|
|
2069
2681
|
setStatus('thinking');
|
|
2070
2682
|
setThinkStart(Date.now());
|
|
2071
2683
|
try {
|
|
2072
|
-
const result = await agent.send(finalText);
|
|
2684
|
+
const result = await agent.send(finalText, images);
|
|
2073
2685
|
if (!result || !result.trim()) {
|
|
2074
2686
|
add('system', 'Agent returned no response.');
|
|
2075
2687
|
}
|
|
@@ -2078,6 +2690,24 @@ function App({ config }) {
|
|
|
2078
2690
|
add('error', e.message || 'Unknown error');
|
|
2079
2691
|
}
|
|
2080
2692
|
finally {
|
|
2693
|
+
// Process queued messages sequentially
|
|
2694
|
+
while (messageQueueRef.current.length > 0) {
|
|
2695
|
+
const next = messageQueueRef.current.shift();
|
|
2696
|
+
const remaining = messageQueueRef.current.length;
|
|
2697
|
+
if (remaining > 0)
|
|
2698
|
+
add('system', `${remaining} more queued`);
|
|
2699
|
+
add('user', next);
|
|
2700
|
+
setStatus('thinking');
|
|
2701
|
+
setThinkStart(Date.now());
|
|
2702
|
+
try {
|
|
2703
|
+
const qResult = await agent.send(next);
|
|
2704
|
+
if (!qResult || !qResult.trim())
|
|
2705
|
+
add('system', 'Agent returned no response.');
|
|
2706
|
+
}
|
|
2707
|
+
catch (e) {
|
|
2708
|
+
add('error', e.message || 'Unknown error');
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2081
2711
|
setIsLoading(false);
|
|
2082
2712
|
setStatus('ready');
|
|
2083
2713
|
setThinkStart(null);
|
|
@@ -2087,6 +2717,7 @@ function App({ config }) {
|
|
|
2087
2717
|
setSessionList(null);
|
|
2088
2718
|
setIsLoading(true);
|
|
2089
2719
|
setStatus('thinking');
|
|
2720
|
+
setThinkStart(Date.now());
|
|
2090
2721
|
loadSession(session.id).then((data) => {
|
|
2091
2722
|
agent.loadMessages(data.messages);
|
|
2092
2723
|
sessionId = session.id;
|
|
@@ -2112,6 +2743,7 @@ function App({ config }) {
|
|
|
2112
2743
|
}).finally(() => {
|
|
2113
2744
|
setIsLoading(false);
|
|
2114
2745
|
setStatus('ready');
|
|
2746
|
+
setThinkStart(null);
|
|
2115
2747
|
});
|
|
2116
2748
|
}, [add]);
|
|
2117
2749
|
const handleAbort = useCallback(() => {
|
|
@@ -2131,7 +2763,7 @@ function App({ config }) {
|
|
|
2131
2763
|
const handleClearScreen = useCallback(() => {
|
|
2132
2764
|
const msgs = agent.getMessages();
|
|
2133
2765
|
if (msgs.length > 0) {
|
|
2134
|
-
saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch(() => { });
|
|
2766
|
+
saveSession(sessionId, msgs, models[0] || 'unknown', undefined, costTracker.getState()).catch((err) => { logger.warn('Failed to save session on clear', { error: err.message }); });
|
|
2135
2767
|
}
|
|
2136
2768
|
agent.clearHistory();
|
|
2137
2769
|
setItems([]);
|
|
@@ -2139,6 +2771,8 @@ function App({ config }) {
|
|
|
2139
2771
|
setShowBanner(true);
|
|
2140
2772
|
costTracker.reset();
|
|
2141
2773
|
setTotalTokens(0);
|
|
2774
|
+
setContextPercent(0);
|
|
2775
|
+
setRateLimitWarn(false);
|
|
2142
2776
|
sessionId = generateSessionId();
|
|
2143
2777
|
}, [models]);
|
|
2144
2778
|
const handleSetupDone = useCallback((newConfig) => {
|
|
@@ -2146,6 +2780,7 @@ function App({ config }) {
|
|
|
2146
2780
|
try {
|
|
2147
2781
|
initAgent(newConfig);
|
|
2148
2782
|
setModels(agent.getModels());
|
|
2783
|
+
setAgentVersion(v => v + 1);
|
|
2149
2784
|
add('system', 'Configuration updated. Agent reinitialized.');
|
|
2150
2785
|
}
|
|
2151
2786
|
catch (err) {
|
|
@@ -2157,8 +2792,18 @@ function App({ config }) {
|
|
|
2157
2792
|
const bannerHeight = showBanner ? 5 : 0;
|
|
2158
2793
|
const statusBarHeight = 1;
|
|
2159
2794
|
const bottomHeight = useMemo(() => {
|
|
2160
|
-
if (confirmInfo)
|
|
2161
|
-
|
|
2795
|
+
if (confirmInfo) {
|
|
2796
|
+
let parsed = {};
|
|
2797
|
+
try {
|
|
2798
|
+
parsed = typeof confirmInfo.args === 'string' ? JSON.parse(confirmInfo.args) : confirmInfo.args ?? {};
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
parsed = {};
|
|
2802
|
+
}
|
|
2803
|
+
const extraCount = Math.min(Object.keys(parsed).filter(k => !['command', 'filePath', 'file_path', 'notebook_path', 'message', 'content'].includes(k)).length, 3);
|
|
2804
|
+
const hasHighlight = !!(parsed.command ?? parsed.filePath ?? parsed.file_path ?? parsed.notebook_path ?? parsed.message);
|
|
2805
|
+
return 5 + (hasHighlight ? 1 : 0) + extraCount;
|
|
2806
|
+
}
|
|
2162
2807
|
if (selectingModel)
|
|
2163
2808
|
return 6 + Math.min(models.length + 2, 12);
|
|
2164
2809
|
if (selectingMode)
|
|
@@ -2178,7 +2823,7 @@ function App({ config }) {
|
|
|
2178
2823
|
const reasonLines = reasoningText ? (reasoningText.match(/\n/g) || []).length + 1 : 0;
|
|
2179
2824
|
return textLines + reasonLines;
|
|
2180
2825
|
}, [isLoading, streamText, reasoningText]);
|
|
2181
|
-
const { visibleItems, scrollUp, scrollDown, scrollToBottom, isAtBottom, } = useScrollableItems(items, chatHeight, cols, isLoading, streamLineCount);
|
|
2826
|
+
const { visibleItems, scrollUp, scrollDown, scrollToBottom, isAtBottom, isAtTop, } = useScrollableItems(items, chatHeight, cols, isLoading, streamLineCount);
|
|
2182
2827
|
const scrollToBottomRef = useRef(scrollToBottom);
|
|
2183
2828
|
scrollToBottomRef.current = scrollToBottom;
|
|
2184
2829
|
const handleSubmitRef = useRef(handleSubmit);
|
|
@@ -2190,14 +2835,14 @@ function App({ config }) {
|
|
|
2190
2835
|
if (showSetup) {
|
|
2191
2836
|
return _jsx(SetupScreen, { onComplete: handleSetupDone, existingConfig: config });
|
|
2192
2837
|
}
|
|
2193
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: rows, children: [showBanner && _jsx(Banner, {}), _jsx(StatusBar, { status: status, model: models[0] || 'no model', toolCount: allTools.length + pluginTools.length, thinkStart: thinkStart, tokens: totalTokens, mode: currentMode }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 6, overflowY: "hidden", children: [showBanner && !isLoading && _jsx(WelcomeScreen, {}), visibleItems.map((item) => _jsx(RenderItem, { item: item }, item.id)), isLoading && _jsx(StreamingArea, { text: streamText, reasoning: reasoningText, model: shortModel(models[0] || '') }), !isAtBottom && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { dimColor: true, children: [" ", '\u2193', " more below ", '\u2014', " Shift+Down "] }) }))] }), _jsx(Box, { flexShrink: 0, flexDirection: "column", children: confirmInfo ? (_jsx(ConfirmPrompt, { toolName: confirmInfo.toolName, args: confirmInfo.args, onRespond: handleConfirm, onScrollUp: scrollUp, onScrollDown: scrollDown })) : selectingModel ? (_jsx(ModelPicker, { currentModels: models, onSelect: (v) => {
|
|
2838
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: rows, children: [showBanner && _jsx(Banner, {}), _jsx(StatusBar, { status: status, model: models[0] || 'no model', toolCount: allTools.length + pluginTools.length, thinkStart: thinkStart, tokens: totalTokens, mode: currentMode, contextPercent: contextPercent, rateLimitWarn: rateLimitWarn }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 6, overflowY: "hidden", children: [showBanner && !isLoading && _jsx(WelcomeScreen, {}), !isAtTop && !showBanner && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { dimColor: true, children: [" ", '\u2191', " more above ", '\u2014', " Shift+Up "] }) })), visibleItems.map((item) => _jsx(RenderItem, { item: item }, item.id)), isLoading && _jsx(StreamingArea, { text: streamText, reasoning: reasoningText, model: shortModel(models[0] || '') }), !isAtBottom && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { dimColor: true, children: [" ", '\u2193', " more below ", '\u2014', " Shift+Down "] }) }))] }), _jsx(Box, { flexShrink: 0, flexDirection: "column", children: confirmInfo ? (_jsx(ConfirmPrompt, { toolName: confirmInfo.toolName, args: confirmInfo.args, onRespond: handleConfirm, onScrollUp: scrollUp, onScrollDown: scrollDown })) : selectingModel ? (_jsx(ModelPicker, { currentModels: models, onSelect: (v) => {
|
|
2194
2839
|
agent.setModel(v);
|
|
2195
2840
|
setModels([v]);
|
|
2196
2841
|
setSelectingModel(false);
|
|
2197
2842
|
loadConfig().then(cfg => {
|
|
2198
2843
|
cfg.models = [v];
|
|
2199
2844
|
saveConfig(cfg);
|
|
2200
|
-
}).catch(() => { });
|
|
2845
|
+
}).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
|
|
2201
2846
|
add('system', `Model switched to ${shortModel(v)}`);
|
|
2202
2847
|
}, onCancel: () => setSelectingModel(false) })) : selectingMode ? (_jsx(ModePicker, { currentMode: currentMode, onSelect: (mode) => {
|
|
2203
2848
|
agent.setPermissionMode(mode);
|
|
@@ -2206,7 +2851,7 @@ function App({ config }) {
|
|
|
2206
2851
|
loadConfig().then(cfg => {
|
|
2207
2852
|
cfg.permissionMode = mode;
|
|
2208
2853
|
saveConfig(cfg);
|
|
2209
|
-
}).catch(() => { });
|
|
2854
|
+
}).catch((err) => { logger.warn('Failed to save config', { error: err.message }); });
|
|
2210
2855
|
add('system', `Mode: ${mode.toUpperCase()} \u2014 ${getModeDescription(mode)}`);
|
|
2211
2856
|
}, onCancel: () => setSelectingMode(false) })) : editingSettings ? (_jsx(SettingsEditor, { onSave: (key, value) => {
|
|
2212
2857
|
loadConfig().then((c) => {
|
|
@@ -2232,7 +2877,7 @@ function App({ config }) {
|
|
|
2232
2877
|
}).catch(() => {
|
|
2233
2878
|
add('error', 'Failed to load config.');
|
|
2234
2879
|
});
|
|
2235
|
-
}, onClose: () => setEditingSettings(false) })) : sessionList ? (_jsx(SessionPicker, { sessions: sessionList, onSelect: handleSessionSelect, onCancel: () => setSessionList(null) })) : (_jsx(InputSection, { onSubmit: handleInputSubmit, disabled: isLoading, onAbort: handleAbort, onClear: handleClearScreen, onScrollUp: scrollUp, onScrollDown: scrollDown, onHeightChange: handleInputHeightChange })) })] }));
|
|
2880
|
+
}, onClose: () => setEditingSettings(false) })) : sessionList ? (_jsx(SessionPicker, { sessions: sessionList, onSelect: handleSessionSelect, onCancel: () => setSessionList(null) })) : (_jsx(InputSection, { onSubmit: handleInputSubmit, disabled: isLoading, onAbort: handleAbort, onClear: handleClearScreen, onScrollUp: scrollUp, onScrollDown: scrollDown, onHeightChange: handleInputHeightChange, onToggleToolGroups: toggleToolGroups, onImagePaste: (dataUrl) => { pendingImagesRef.current.push(dataUrl); } })) })] }));
|
|
2236
2881
|
}
|
|
2237
2882
|
// ─── Root (config loading + setup gate) ───
|
|
2238
2883
|
function Root() {
|
|
@@ -2247,7 +2892,10 @@ function Root() {
|
|
|
2247
2892
|
setConfig(c);
|
|
2248
2893
|
if (isConfigured(c)) {
|
|
2249
2894
|
// Fetch model specs from API in background (uses user-defined/built-in as fallback)
|
|
2250
|
-
fetchModelSpecs(c.baseUrl, c.apiKey).catch(() => { });
|
|
2895
|
+
fetchModelSpecs(c.baseUrl, c.apiKey).catch((err) => { logger.warn('Failed to fetch model specs', { error: err.message }); });
|
|
2896
|
+
if (!process.env.NO_UPDATE_NOTIFIER && !c.disableUpdateCheck) {
|
|
2897
|
+
checkForUpdate().catch(() => { });
|
|
2898
|
+
}
|
|
2251
2899
|
try {
|
|
2252
2900
|
initAgent(c);
|
|
2253
2901
|
setReady(true);
|
|
@@ -2257,7 +2905,7 @@ function Root() {
|
|
|
2257
2905
|
}
|
|
2258
2906
|
}
|
|
2259
2907
|
}).catch(() => {
|
|
2260
|
-
setConfig({ apiKey: '', baseUrl: DEFAULT_BASE_URL, models: [...DEFAULT_MODELS], customModels: {}, limits: { ...DEFAULT_LIMITS }, permissionMode: 'default', thinkingMode: 'off' });
|
|
2908
|
+
setConfig({ apiKey: '', baseUrl: DEFAULT_BASE_URL, models: [...DEFAULT_MODELS], customModels: {}, limits: { ...DEFAULT_LIMITS }, permissionMode: 'default', thinkingMode: 'off', security: {}, injectionDetection: { enabled: true, sensitivity: 'medium' } });
|
|
2261
2909
|
});
|
|
2262
2910
|
}, []);
|
|
2263
2911
|
const handleSetupComplete = useCallback((c) => {
|
|
@@ -2324,6 +2972,6 @@ function _restoreTerminal() {
|
|
|
2324
2972
|
_origWrite('\x1b[?25h'); // show cursor
|
|
2325
2973
|
}
|
|
2326
2974
|
process.on('exit', _restoreTerminal);
|
|
2327
|
-
process.on('SIGINT', () => { _restoreTerminal(); process.exit(0); });
|
|
2328
|
-
process.on('SIGTERM', () => { _restoreTerminal(); process.exit(0); });
|
|
2975
|
+
process.on('SIGINT', () => { _restoreTerminal(); closeBrowser().finally(() => process.exit(0)); });
|
|
2976
|
+
process.on('SIGTERM', () => { _restoreTerminal(); closeBrowser().finally(() => process.exit(0)); });
|
|
2329
2977
|
render(_jsx(Root, {}), { exitOnCtrlC: false, patchConsole: false });
|