scalix 1.1.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/PARITY-PLAN.md +116 -0
- package/bin/scalix.js +1 -0
- package/dist/agent.js +12 -1
- package/dist/cli.js +567 -44
- package/dist/commands.js +53 -2
- package/dist/constants.js +3 -3
- package/dist/headless.js +42 -2
- package/dist/hooks.js +141 -9
- package/dist/keychain.js +109 -0
- package/dist/markdown.js +46 -3
- package/dist/plugins.js +132 -0
- package/dist/project-index.js +20 -1
- package/dist/sandbox.js +66 -0
- package/dist/sessions.js +59 -0
- package/dist/settings.js +85 -4
- package/dist/skills.js +269 -0
- package/dist/terminal-notify.js +73 -0
- package/dist/text-utils.js +146 -0
- package/dist/tools-extra.js +2 -8
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ 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';
|
|
@@ -21,23 +22,26 @@ 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';
|
|
27
28
|
import { checkForUpdate, getUpdateBanner, performUpdate } from './update-checker.js';
|
|
28
29
|
import { loadConfig, saveConfig, isConfigured, DEFAULT_LIMITS, THINKING_MODES } from './settings.js';
|
|
29
|
-
import { getModeDescription, PERMISSION_MODES, setSandboxRoot } from './sandbox.js';
|
|
30
|
+
import { getModeDescription, PERMISSION_MODES, setSandboxRoot, checkScopedPermission, getScopedPermissionRules } from './sandbox.js';
|
|
30
31
|
import { buildProjectIndex, indexToContext } from './project-index.js';
|
|
31
32
|
import { collectGitInfo, gitInfoToContext } from './git-context.js';
|
|
32
33
|
import { loadMemory, autoDetectConventions, saveMemory, memoryToContext, addNote, detectCorrection } from './project-memory.js';
|
|
33
|
-
import { discoverPlugins, getPluginTools, pluginsSummary } from './plugins.js';
|
|
34
|
+
import { discoverPlugins, getPluginTools, pluginsSummary, installPlugin, uninstallPlugin, listInstalledPlugins, enablePlugin, disablePlugin } from './plugins.js';
|
|
34
35
|
import { createRouterConfig, getRouterStatus } from './model-router.js';
|
|
35
36
|
import { loadCustomCommands, expandCommand, commandsSummary } from './commands.js';
|
|
36
37
|
import { hooksSummary } from './hooks.js';
|
|
37
38
|
import { spawnSubAgent, pickCheapestModel } from './sub-agent.js';
|
|
38
39
|
import { createCheckpoint, undoLastCheckpoint, listCheckpoints } from './git-undo.js';
|
|
39
40
|
import { loadScopedRules, rulesToContext } from './scoped-rules.js';
|
|
41
|
+
import { loadSkills, skillsSummary, getSkillByTrigger, getSkillContent } from './skills.js';
|
|
40
42
|
import { AgentRegistry } from './agent-registry.js';
|
|
43
|
+
import { createWorktree, listWorktrees, removeWorktree, cleanupStaleWorktrees } from './worktree.js';
|
|
44
|
+
import { sendNotification, getTerminalType } from './terminal-notify.js';
|
|
41
45
|
logger.setLevel('silent');
|
|
42
46
|
setSandboxRoot(process.cwd());
|
|
43
47
|
// Suppress noisy React 19 development-mode reconciler warnings that leak to stderr
|
|
@@ -57,10 +61,21 @@ const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools,
|
|
|
57
61
|
import { EventEmitter } from 'node:events';
|
|
58
62
|
const confirmEmitter = new EventEmitter();
|
|
59
63
|
let pendingConfirm = null;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
};
|
|
64
79
|
// ─── Agent (deferred until config is ready) ───
|
|
65
80
|
let baseInstructions = DEFAULT_INSTRUCTIONS;
|
|
66
81
|
let agent = null;
|
|
@@ -69,6 +84,7 @@ let projectIndex = null;
|
|
|
69
84
|
let loadedPlugins = [];
|
|
70
85
|
let projectMemory = null;
|
|
71
86
|
let customCommands = [];
|
|
87
|
+
let loadedSkills = [];
|
|
72
88
|
async function buildEnhancedInstructions() {
|
|
73
89
|
let instructions = baseInstructions;
|
|
74
90
|
const cwd = process.cwd();
|
|
@@ -154,6 +170,9 @@ function initAgent(config) {
|
|
|
154
170
|
loadCustomCommands(process.cwd()).then((cmds) => {
|
|
155
171
|
customCommands = cmds;
|
|
156
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 }); });
|
|
157
176
|
return agent;
|
|
158
177
|
}
|
|
159
178
|
// ─── Helpers ───
|
|
@@ -204,6 +223,43 @@ const Banner = memo(function Banner() {
|
|
|
204
223
|
function sanitizeInput(v) {
|
|
205
224
|
return v.replace(/\[20[01]~/g, '').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
|
206
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
|
+
}
|
|
207
263
|
function LongTextInput({ value, onChange, onSubmit, placeholder, maxVisible = 80 }) {
|
|
208
264
|
const valueRef = useRef(value);
|
|
209
265
|
useEffect(() => { valueRef.current = value; }, [value]);
|
|
@@ -387,7 +443,7 @@ function SetupScreen({ onComplete, existingConfig }) {
|
|
|
387
443
|
});
|
|
388
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"] }) })] })] }));
|
|
389
445
|
}
|
|
390
|
-
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 }) {
|
|
391
447
|
const { cols } = useTermSize();
|
|
392
448
|
const [elapsedStr, setElapsedStr] = useState('');
|
|
393
449
|
useEffect(() => {
|
|
@@ -410,10 +466,13 @@ const StatusBar = memo(function StatusBar({ status, model, toolCount, thinkStart
|
|
|
410
466
|
leftParts.push(`${toolCount} tools`);
|
|
411
467
|
if (activeAgents > 0)
|
|
412
468
|
leftParts.push(`${activeAgents} agent${activeAgents > 1 ? 's' : ''}`);
|
|
469
|
+
if (contextPercent > 50)
|
|
470
|
+
leftParts.push(`ctx ${contextPercent}%`);
|
|
413
471
|
const leftText = leftParts.join(' \u00B7 ');
|
|
414
|
-
const
|
|
472
|
+
const warnText = rateLimitWarn ? ' \u26A0 Rate limited' : '';
|
|
473
|
+
const rightLen = mode.length + VERSION.length + 6 + (elapsedStr ? elapsedStr.length + 3 : 0) + warnText.length;
|
|
415
474
|
const gap = Math.max(cols - leftText.length - rightLen - 8, 2);
|
|
416
|
-
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] })] })] }));
|
|
417
476
|
});
|
|
418
477
|
const WelcomeScreen = memo(function WelcomeScreen() {
|
|
419
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)))] })] }));
|
|
@@ -456,6 +515,12 @@ const ToolResultMessage = memo(function ToolResultMessage({ name, result }) {
|
|
|
456
515
|
const ToolBlockedMessage = memo(function ToolBlockedMessage({ text }) {
|
|
457
516
|
return (_jsxs(Box, { paddingLeft: 3, paddingRight: 2, gap: 1, children: [_jsx(Text, { color: "red", children: '\u25CF' }), _jsx(Text, { color: "red", children: text })] }));
|
|
458
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
|
+
});
|
|
459
524
|
const DIFF_MAX_LINES = 60;
|
|
460
525
|
const DiffMessage = memo(function DiffMessage({ text, meta }) {
|
|
461
526
|
const allLines = text.split('\n');
|
|
@@ -491,6 +556,7 @@ const RenderItem = memo(function RenderItem({ item }) {
|
|
|
491
556
|
case 'tool-call': return _jsx(ToolCallMessage, { name: item.meta ?? '', args: item.text });
|
|
492
557
|
case 'tool-result': return _jsx(ToolResultMessage, { name: item.meta ?? '', result: item.text });
|
|
493
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 });
|
|
494
560
|
case 'diff': return _jsx(DiffMessage, { text: item.text, meta: item.meta });
|
|
495
561
|
case 'system': return _jsx(SystemMessage, { text: item.text });
|
|
496
562
|
}
|
|
@@ -519,14 +585,20 @@ const StreamingArea = memo(function StreamingArea({ text, reasoning, model, maxL
|
|
|
519
585
|
function ConfirmPrompt({ toolName, args, onRespond, onScrollUp, onScrollDown }) {
|
|
520
586
|
const { cols } = useTermSize();
|
|
521
587
|
useInput((ch, key) => {
|
|
522
|
-
if (ch.toLowerCase() === '
|
|
588
|
+
if (ch.toLowerCase() === 'a')
|
|
589
|
+
onRespond(true, true);
|
|
590
|
+
else if (ch.toLowerCase() === 'y')
|
|
523
591
|
onRespond(true);
|
|
524
592
|
else if (ch.toLowerCase() === 'n' || key.escape)
|
|
525
593
|
onRespond(false);
|
|
526
|
-
else if (key.pageUp
|
|
527
|
-
onScrollUp?.();
|
|
528
|
-
else if (key.
|
|
529
|
-
|
|
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);
|
|
530
602
|
});
|
|
531
603
|
let parsed = {};
|
|
532
604
|
try {
|
|
@@ -547,7 +619,7 @@ function ConfirmPrompt({ toolName, args, onRespond, onScrollUp, onScrollDown })
|
|
|
547
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]) => {
|
|
548
620
|
const val = truncate(String(v), boxWidth - k.length - 6);
|
|
549
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));
|
|
550
|
-
}), _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: "red", bold: true, children: "[N]" }), _jsx(Text, { children: " Deny " }), _jsx(Text, { dimColor: true, children: "ESC
|
|
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'] })] }));
|
|
551
623
|
}
|
|
552
624
|
function ModelPicker({ currentModels, onSelect, onCancel }) {
|
|
553
625
|
const [customInput, setCustomInput] = useState('');
|
|
@@ -733,7 +805,7 @@ function SettingsEditor({ onSave, onClose }) {
|
|
|
733
805
|
}
|
|
734
806
|
} })] }));
|
|
735
807
|
}
|
|
736
|
-
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 }) {
|
|
737
809
|
const { exit } = useApp();
|
|
738
810
|
const { cols } = useTermSize();
|
|
739
811
|
const line = useMemo(() => '\u2500'.repeat(Math.max(cols - 6, 20)), [cols]);
|
|
@@ -744,7 +816,11 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
744
816
|
const [history, setHistory] = useState([]);
|
|
745
817
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
746
818
|
const savedInput = useRef('');
|
|
819
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
820
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
821
|
+
const ctrlXPending = useRef(false);
|
|
747
822
|
const valueRef = useRef('');
|
|
823
|
+
const [imageChips, setImageChips] = useState([]);
|
|
748
824
|
const onChange = useCallback((v) => {
|
|
749
825
|
setValue(v);
|
|
750
826
|
valueRef.current = v;
|
|
@@ -814,6 +890,88 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
814
890
|
onChange('');
|
|
815
891
|
return;
|
|
816
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
|
+
}
|
|
817
975
|
// ── Ctrl+S: stash / restore prompt ──
|
|
818
976
|
if (ch === 's' && key.ctrl && !disabled) {
|
|
819
977
|
if (val.length > 0) {
|
|
@@ -826,6 +984,16 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
826
984
|
}
|
|
827
985
|
return;
|
|
828
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
|
+
}
|
|
829
997
|
// ── Tab: autocomplete slash commands ──
|
|
830
998
|
if (key.tab && !disabled && val.startsWith('/') && !val.includes(' ')) {
|
|
831
999
|
const cmdNames = commands.map((c) => c.cmd);
|
|
@@ -859,12 +1027,20 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
859
1027
|
return;
|
|
860
1028
|
}
|
|
861
1029
|
// ── Scroll ──
|
|
862
|
-
if (key.pageUp
|
|
863
|
-
onScrollUp();
|
|
1030
|
+
if (key.pageUp) {
|
|
1031
|
+
onScrollUp(15);
|
|
864
1032
|
return;
|
|
865
1033
|
}
|
|
866
|
-
if (key.
|
|
867
|
-
|
|
1034
|
+
if (key.upArrow && key.shift) {
|
|
1035
|
+
onScrollUp(5);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (key.pageDown) {
|
|
1039
|
+
onScrollDown(15);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (key.downArrow && key.shift) {
|
|
1043
|
+
onScrollDown(5);
|
|
868
1044
|
return;
|
|
869
1045
|
}
|
|
870
1046
|
// ── History navigation ──
|
|
@@ -906,6 +1082,8 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
906
1082
|
{ cmd: '/copy', desc: 'Copy last response to clipboard' },
|
|
907
1083
|
{ cmd: '/save', desc: 'Save current session' },
|
|
908
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' },
|
|
909
1087
|
{ cmd: '/export', desc: 'Export conversation as text' },
|
|
910
1088
|
{ cmd: '/index', desc: 'Scan & index project files' },
|
|
911
1089
|
{ cmd: '/init', desc: 'Initialize project config (SCALIX.md)' },
|
|
@@ -920,9 +1098,11 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
920
1098
|
{ cmd: '/mcp', desc: 'Add MCP server' },
|
|
921
1099
|
{ cmd: '/setup', desc: 'Reconfigure API key, URL & models' },
|
|
922
1100
|
{ cmd: '/settings', desc: 'View & configure limits' },
|
|
1101
|
+
{ cmd: '/permissions', desc: 'View permission rules & recent denials' },
|
|
923
1102
|
{ cmd: '/shortcuts', desc: 'Keyboard shortcuts' },
|
|
924
1103
|
{ cmd: '/hooks', desc: 'Show lifecycle hooks' },
|
|
925
1104
|
{ cmd: '/commands', desc: 'Show custom commands' },
|
|
1105
|
+
{ cmd: '/skills', desc: 'List available skills' },
|
|
926
1106
|
{ cmd: '/delegate', desc: 'Run task with sub-agent' },
|
|
927
1107
|
{ cmd: '/agents', desc: 'Show active agents & usage' },
|
|
928
1108
|
{ cmd: '/checkpoint', desc: 'Create git checkpoint' },
|
|
@@ -932,6 +1112,9 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
932
1112
|
{ cmd: '/review', desc: 'AI code review of current changes' },
|
|
933
1113
|
{ cmd: '/watch', desc: 'Run command, auto-fix on failure' },
|
|
934
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' },
|
|
935
1118
|
{ cmd: '/exit', desc: 'Save & exit' },
|
|
936
1119
|
...customCommands.map((c) => ({ cmd: `/${c.name}`, desc: c.description })),
|
|
937
1120
|
], [customCommands]);
|
|
@@ -940,18 +1123,37 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
940
1123
|
const filteredCommands = showCommandPicker
|
|
941
1124
|
? commands.filter((c) => c.cmd.startsWith(prefix))
|
|
942
1125
|
: [];
|
|
943
|
-
|
|
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);
|
|
944
1145
|
useEffect(() => {
|
|
945
1146
|
if (!onHeightChange)
|
|
946
1147
|
return;
|
|
947
1148
|
let h = 3; // separator + input row + padding
|
|
1149
|
+
const pickerCount = showCommandPicker ? filteredCommands.length : filteredFiles.length;
|
|
948
1150
|
if (pickerActive)
|
|
949
|
-
h += Math.min(
|
|
1151
|
+
h += Math.min(pickerCount, 10) + 2;
|
|
950
1152
|
if (stash.length > 0 && !disabled)
|
|
951
1153
|
h += 1;
|
|
952
1154
|
onHeightChange(h);
|
|
953
|
-
}, [pickerActive, filteredCommands.length, stash.length, disabled, onHeightChange]);
|
|
954
|
-
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) => ({
|
|
955
1157
|
label: `${c.cmd.padEnd(16)} ${c.desc}`,
|
|
956
1158
|
value: c.cmd,
|
|
957
1159
|
})), isFocused: true, onSelect: (item) => {
|
|
@@ -963,7 +1165,14 @@ const InputSection = memo(function InputSection({ onSubmit, disabled, onAbort, o
|
|
|
963
1165
|
onChange('');
|
|
964
1166
|
handleSubmit(item.value);
|
|
965
1167
|
}
|
|
966
|
-
}, 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" }) }))] }) })] }));
|
|
967
1176
|
});
|
|
968
1177
|
// ─── App ───
|
|
969
1178
|
function estimateLines(item, cols) {
|
|
@@ -983,12 +1192,14 @@ function estimateLines(item, cols) {
|
|
|
983
1192
|
return totalLines + markdownExpansion + 2; // marginTop=1 + padding
|
|
984
1193
|
}
|
|
985
1194
|
case 'error': return totalLines + 2;
|
|
986
|
-
case 'tool-call': return 2;
|
|
987
|
-
case 'tool-result': return 1;
|
|
1195
|
+
case 'tool-call': return 2;
|
|
1196
|
+
case 'tool-result': return Math.max(1, totalLines);
|
|
988
1197
|
case 'tool-blocked': return 1;
|
|
989
|
-
case '
|
|
990
|
-
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);
|
|
991
1201
|
}
|
|
1202
|
+
return totalLines;
|
|
992
1203
|
}
|
|
993
1204
|
function useScrollableItems(items, availableHeight, cols, isLoading, streamLineCount = 0) {
|
|
994
1205
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
@@ -1004,7 +1215,7 @@ function useScrollableItems(items, availableHeight, cols, isLoading, streamLineC
|
|
|
1004
1215
|
if (isLoading && autoScroll.current)
|
|
1005
1216
|
setScrollOffset(0);
|
|
1006
1217
|
}, [isLoading]);
|
|
1007
|
-
const scrollUp = useCallback((amount =
|
|
1218
|
+
const scrollUp = useCallback((amount = 5) => {
|
|
1008
1219
|
setScrollOffset((prev) => {
|
|
1009
1220
|
const maxOffset = Math.max(items.length - 1, 0);
|
|
1010
1221
|
const next = Math.min(prev + amount, maxOffset);
|
|
@@ -1012,7 +1223,7 @@ function useScrollableItems(items, availableHeight, cols, isLoading, streamLineC
|
|
|
1012
1223
|
return next;
|
|
1013
1224
|
});
|
|
1014
1225
|
}, [items.length]);
|
|
1015
|
-
const scrollDown = useCallback((amount =
|
|
1226
|
+
const scrollDown = useCallback((amount = 5) => {
|
|
1016
1227
|
setScrollOffset((prev) => {
|
|
1017
1228
|
const next = Math.max(prev - amount, 0);
|
|
1018
1229
|
if (next === 0)
|
|
@@ -1053,6 +1264,8 @@ function App({ config }) {
|
|
|
1053
1264
|
const [thinkStart, setThinkStart] = useState(null);
|
|
1054
1265
|
const [showBanner, setShowBanner] = useState(true);
|
|
1055
1266
|
const [totalTokens, setTotalTokens] = useState(0);
|
|
1267
|
+
const [contextPercent, setContextPercent] = useState(0);
|
|
1268
|
+
const [rateLimitWarn, setRateLimitWarn] = useState(false);
|
|
1056
1269
|
const [sessionList, setSessionList] = useState(null);
|
|
1057
1270
|
const [showSetup, setShowSetup] = useState(false);
|
|
1058
1271
|
const [currentMode, setCurrentMode] = useState(agent?.getPermissionMode() ?? 'default');
|
|
@@ -1065,6 +1278,7 @@ function App({ config }) {
|
|
|
1065
1278
|
useEffect(() => { modelsRef.current = models; }, [models]);
|
|
1066
1279
|
const nextId = () => ++counter.current;
|
|
1067
1280
|
const MAX_DISPLAY_ITEMS = 2000;
|
|
1281
|
+
const TOOL_GROUP_THRESHOLD = 3;
|
|
1068
1282
|
const add = useCallback((kind, text, meta, hint) => {
|
|
1069
1283
|
setItems((prev) => {
|
|
1070
1284
|
const next = [...prev, { id: nextId(), kind, text, meta, hint }];
|
|
@@ -1075,6 +1289,29 @@ function App({ config }) {
|
|
|
1075
1289
|
setShowBanner(false);
|
|
1076
1290
|
}
|
|
1077
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
|
+
}, []);
|
|
1078
1315
|
// Register agent event handlers ONCE
|
|
1079
1316
|
useEffect(() => {
|
|
1080
1317
|
const onThinkingStart = () => {
|
|
@@ -1126,9 +1363,18 @@ function App({ config }) {
|
|
|
1126
1363
|
streamBuffer = '';
|
|
1127
1364
|
setStreamText('');
|
|
1128
1365
|
setReasoningText('');
|
|
1366
|
+
collapseToolGroups();
|
|
1129
1367
|
if (full.trim()) {
|
|
1130
1368
|
add('assistant', full);
|
|
1131
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
|
+
}
|
|
1132
1378
|
};
|
|
1133
1379
|
let reasoningBuffer = '';
|
|
1134
1380
|
let reasoningFlushTimer = null;
|
|
@@ -1198,6 +1444,8 @@ function App({ config }) {
|
|
|
1198
1444
|
const lower = reason?.toLowerCase() || '';
|
|
1199
1445
|
if (lower.includes('rate limit') || lower.includes('429')) {
|
|
1200
1446
|
hint = 'Rate limited. Try /model to switch models, or wait a moment.';
|
|
1447
|
+
setRateLimitWarn(true);
|
|
1448
|
+
setTimeout(() => setRateLimitWarn(false), 120_000);
|
|
1201
1449
|
}
|
|
1202
1450
|
else if (lower.includes('auth') || lower.includes('401') || lower.includes('api key')) {
|
|
1203
1451
|
hint = 'Authentication failed. Run /setup to reconfigure your API key.';
|
|
@@ -1217,6 +1465,7 @@ function App({ config }) {
|
|
|
1217
1465
|
add('system', `Context compressed: ${formatTokens(info.saved)} tokens saved (${formatTokens(info.before)} \u2192 ${formatTokens(info.after)})`);
|
|
1218
1466
|
};
|
|
1219
1467
|
const onContextUsage = (info) => {
|
|
1468
|
+
setContextPercent(info.percentage);
|
|
1220
1469
|
if (info.percentage > 80) {
|
|
1221
1470
|
add('system', `Context usage: ${info.percentage}% (${formatTokens(info.tokens)}/${formatTokens(info.maxTokens)} tokens)`);
|
|
1222
1471
|
}
|
|
@@ -1264,6 +1513,40 @@ function App({ config }) {
|
|
|
1264
1513
|
currentAgent.off('context:usage', onContextUsage);
|
|
1265
1514
|
};
|
|
1266
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(() => { });
|
|
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]);
|
|
1267
1550
|
// Confirmation bridge (event-driven)
|
|
1268
1551
|
useEffect(() => {
|
|
1269
1552
|
const onPending = () => {
|
|
@@ -1279,8 +1562,19 @@ function App({ config }) {
|
|
|
1279
1562
|
if (banner)
|
|
1280
1563
|
add('system', banner);
|
|
1281
1564
|
}, [add]);
|
|
1282
|
-
const handleConfirm = useCallback((ok) => {
|
|
1565
|
+
const handleConfirm = useCallback((ok, always) => {
|
|
1283
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
|
+
}
|
|
1284
1578
|
pendingConfirm.resolve(ok);
|
|
1285
1579
|
pendingConfirm = null;
|
|
1286
1580
|
}
|
|
@@ -1318,6 +1612,7 @@ function App({ config }) {
|
|
|
1318
1612
|
' /checkpoint \u2014 create git checkpoint',
|
|
1319
1613
|
' /undo \u2014 undo last checkpoint',
|
|
1320
1614
|
' /checkpoints \u2014 list recent checkpoints',
|
|
1615
|
+
' /worktree \u2014 manage git worktrees (list/create/remove/cleanup)',
|
|
1321
1616
|
' /git \u2014 show git context',
|
|
1322
1617
|
'',
|
|
1323
1618
|
'Project:',
|
|
@@ -1339,6 +1634,8 @@ function App({ config }) {
|
|
|
1339
1634
|
' /setup \u2014 reconfigure API key, URL & models',
|
|
1340
1635
|
' /mcp <l> <u> \u2014 add MCP server',
|
|
1341
1636
|
' /plugins \u2014 show loaded plugins',
|
|
1637
|
+
' /plugin \u2014 install/enable/disable/uninstall plugins',
|
|
1638
|
+
' /notify \u2014 test terminal notification',
|
|
1342
1639
|
' /router \u2014 show model routing status',
|
|
1343
1640
|
' /replay \u2014 show session timeline',
|
|
1344
1641
|
' /hooks \u2014 show lifecycle hooks',
|
|
@@ -1380,6 +1677,8 @@ function App({ config }) {
|
|
|
1380
1677
|
setShowBanner(true);
|
|
1381
1678
|
costTracker.reset();
|
|
1382
1679
|
setTotalTokens(0);
|
|
1680
|
+
setContextPercent(0);
|
|
1681
|
+
setRateLimitWarn(false);
|
|
1383
1682
|
sessionId = generateSessionId();
|
|
1384
1683
|
return true;
|
|
1385
1684
|
}
|
|
@@ -1432,7 +1731,35 @@ function App({ config }) {
|
|
|
1432
1731
|
});
|
|
1433
1732
|
return true;
|
|
1434
1733
|
}
|
|
1435
|
-
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
|
+
}
|
|
1436
1763
|
listSessions().then((sessions) => {
|
|
1437
1764
|
if (sessions.length === 0) {
|
|
1438
1765
|
add('system', 'No saved sessions found.');
|
|
@@ -1443,20 +1770,43 @@ function App({ config }) {
|
|
|
1443
1770
|
});
|
|
1444
1771
|
return true;
|
|
1445
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
|
+
}
|
|
1446
1789
|
if (cmd === '/shortcuts') {
|
|
1447
1790
|
const shortcuts = [
|
|
1448
1791
|
'Ctrl+C \u2014 interrupt agent / clear input / double-press to quit',
|
|
1449
1792
|
'Ctrl+D \u2014 exit (when input is empty)',
|
|
1450
1793
|
'Ctrl+L \u2014 clear screen & conversation',
|
|
1794
|
+
'Ctrl+O \u2014 toggle collapsed tool groups',
|
|
1451
1795
|
'Ctrl+U \u2014 clear entire input line',
|
|
1452
1796
|
'Ctrl+W \u2014 delete last word',
|
|
1453
1797
|
'Ctrl+K \u2014 delete to end of line',
|
|
1454
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)',
|
|
1455
1803
|
'Tab \u2014 autocomplete slash commands',
|
|
1456
1804
|
'\u2191/\u2193 \u2014 navigate input history',
|
|
1457
|
-
'Shift+\u2191
|
|
1458
|
-
'Shift+\u2193
|
|
1459
|
-
'
|
|
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',
|
|
1460
1810
|
'Esc \u2014 quit',
|
|
1461
1811
|
'',
|
|
1462
1812
|
'Copy \u2014 Cmd+C (Mac) / Ctrl+Shift+C (Linux)',
|
|
@@ -1468,6 +1818,32 @@ function App({ config }) {
|
|
|
1468
1818
|
shortcuts.forEach((s) => add('system', s));
|
|
1469
1819
|
return true;
|
|
1470
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
|
+
}
|
|
1471
1847
|
if (cmd === '/settings') {
|
|
1472
1848
|
setEditingSettings(true);
|
|
1473
1849
|
return true;
|
|
@@ -1610,6 +1986,10 @@ function App({ config }) {
|
|
|
1610
1986
|
commandsSummary(customCommands).split('\n').forEach((l) => add('system', l));
|
|
1611
1987
|
return true;
|
|
1612
1988
|
}
|
|
1989
|
+
if (cmd === '/skills') {
|
|
1990
|
+
skillsSummary(loadedSkills).split('\n').forEach((l) => add('system', l));
|
|
1991
|
+
return true;
|
|
1992
|
+
}
|
|
1613
1993
|
if (cmd === '/delegate') {
|
|
1614
1994
|
const prompt = text.slice(10).trim();
|
|
1615
1995
|
if (!prompt) {
|
|
@@ -1677,6 +2057,130 @@ function App({ config }) {
|
|
|
1677
2057
|
}).catch(() => add('error', 'Failed to list checkpoints.'));
|
|
1678
2058
|
return true;
|
|
1679
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
|
+
}
|
|
1680
2184
|
if (cmd === '/commit') {
|
|
1681
2185
|
setIsLoading(true);
|
|
1682
2186
|
setStatus('thinking');
|
|
@@ -1715,7 +2219,8 @@ function App({ config }) {
|
|
|
1715
2219
|
}
|
|
1716
2220
|
add('system', `Commit message: ${cleanMsg}`);
|
|
1717
2221
|
try {
|
|
1718
|
-
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();
|
|
1719
2224
|
add('system', result);
|
|
1720
2225
|
}
|
|
1721
2226
|
catch (e) {
|
|
@@ -2096,10 +2601,14 @@ function App({ config }) {
|
|
|
2096
2601
|
useEffect(() => { isLoadingRef.current = isLoading; }, [isLoading]);
|
|
2097
2602
|
const multilineRef = useRef([]);
|
|
2098
2603
|
const messageQueueRef = useRef([]);
|
|
2604
|
+
const pendingImagesRef = useRef([]);
|
|
2099
2605
|
const handleSubmit = useCallback(async (text) => {
|
|
2100
2606
|
const trimmed = text.trim();
|
|
2101
2607
|
if (!trimmed)
|
|
2102
2608
|
return;
|
|
2609
|
+
// Reset idle nudge on any user activity
|
|
2610
|
+
lastActivityRef.current = Date.now();
|
|
2611
|
+
idleNudgedRef.current = false;
|
|
2103
2612
|
// Allow /mode even while the agent is working
|
|
2104
2613
|
if (isLoadingRef.current && trimmed.startsWith('/mode')) {
|
|
2105
2614
|
handleCommand(trimmed);
|
|
@@ -2132,6 +2641,11 @@ function App({ config }) {
|
|
|
2132
2641
|
}
|
|
2133
2642
|
if (finalText.startsWith('/') && handleCommand(finalText))
|
|
2134
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
|
+
}
|
|
2135
2649
|
if (finalText.startsWith('!')) {
|
|
2136
2650
|
const shellCmd = finalText.slice(1).trim();
|
|
2137
2651
|
if (!shellCmd)
|
|
@@ -2155,12 +2669,19 @@ function App({ config }) {
|
|
|
2155
2669
|
if (correction) {
|
|
2156
2670
|
addNote(process.cwd(), `[correction] ${correction}`).catch((err) => { logger.warn('Failed to save correction note', { error: err.message }); });
|
|
2157
2671
|
}
|
|
2158
|
-
|
|
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]`);
|
|
2675
|
+
}
|
|
2676
|
+
else {
|
|
2677
|
+
add('user', finalText);
|
|
2678
|
+
}
|
|
2679
|
+
pendingImagesRef.current = [];
|
|
2159
2680
|
setIsLoading(true);
|
|
2160
2681
|
setStatus('thinking');
|
|
2161
2682
|
setThinkStart(Date.now());
|
|
2162
2683
|
try {
|
|
2163
|
-
const result = await agent.send(finalText);
|
|
2684
|
+
const result = await agent.send(finalText, images);
|
|
2164
2685
|
if (!result || !result.trim()) {
|
|
2165
2686
|
add('system', 'Agent returned no response.');
|
|
2166
2687
|
}
|
|
@@ -2250,6 +2771,8 @@ function App({ config }) {
|
|
|
2250
2771
|
setShowBanner(true);
|
|
2251
2772
|
costTracker.reset();
|
|
2252
2773
|
setTotalTokens(0);
|
|
2774
|
+
setContextPercent(0);
|
|
2775
|
+
setRateLimitWarn(false);
|
|
2253
2776
|
sessionId = generateSessionId();
|
|
2254
2777
|
}, [models]);
|
|
2255
2778
|
const handleSetupDone = useCallback((newConfig) => {
|
|
@@ -2300,7 +2823,7 @@ function App({ config }) {
|
|
|
2300
2823
|
const reasonLines = reasoningText ? (reasoningText.match(/\n/g) || []).length + 1 : 0;
|
|
2301
2824
|
return textLines + reasonLines;
|
|
2302
2825
|
}, [isLoading, streamText, reasoningText]);
|
|
2303
|
-
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);
|
|
2304
2827
|
const scrollToBottomRef = useRef(scrollToBottom);
|
|
2305
2828
|
scrollToBottomRef.current = scrollToBottom;
|
|
2306
2829
|
const handleSubmitRef = useRef(handleSubmit);
|
|
@@ -2312,7 +2835,7 @@ function App({ config }) {
|
|
|
2312
2835
|
if (showSetup) {
|
|
2313
2836
|
return _jsx(SetupScreen, { onComplete: handleSetupDone, existingConfig: config });
|
|
2314
2837
|
}
|
|
2315
|
-
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) => {
|
|
2316
2839
|
agent.setModel(v);
|
|
2317
2840
|
setModels([v]);
|
|
2318
2841
|
setSelectingModel(false);
|
|
@@ -2354,7 +2877,7 @@ function App({ config }) {
|
|
|
2354
2877
|
}).catch(() => {
|
|
2355
2878
|
add('error', 'Failed to load config.');
|
|
2356
2879
|
});
|
|
2357
|
-
}, 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); } })) })] }));
|
|
2358
2881
|
}
|
|
2359
2882
|
// ─── Root (config loading + setup gate) ───
|
|
2360
2883
|
function Root() {
|