praxis-agent 0.9.0 → 0.11.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/README.md +18 -4
- package/dist/application/session-service.d.ts +5 -2
- package/dist/application/session-service.js +23 -0
- package/dist/application/subagent-service.d.ts +4 -2
- package/dist/application/subagent-service.js +4 -0
- package/dist/cli/interactive.d.ts +4 -3
- package/dist/cli/interactive.js +760 -195
- package/dist/cli/tui/claude-command-inventory.d.ts +368 -0
- package/dist/cli/tui/claude-command-inventory.js +220 -0
- package/dist/cli/tui/claude-style.d.ts +12 -14
- package/dist/cli/tui/claude-style.js +45 -67
- package/dist/cli/tui/config-dashboard.d.ts +4 -8
- package/dist/cli/tui/config-dashboard.js +4 -18
- package/dist/cli/tui/config-settings.js +1 -1
- package/dist/cli/tui/conversation-export.d.ts +1 -1
- package/dist/cli/tui/conversation-export.js +2 -11
- package/dist/cli/tui/mcp-elicitation.d.ts +75 -0
- package/dist/cli/tui/mcp-elicitation.js +497 -0
- package/dist/cli/tui/open-url.d.ts +2 -0
- package/dist/cli/tui/open-url.js +13 -0
- package/dist/cli/tui/permission-settings.d.ts +6 -0
- package/dist/cli/tui/permission-settings.js +84 -0
- package/dist/cli/tui/runtime-interactions.d.ts +2 -0
- package/dist/cli/tui/runtime-interactions.js +44 -17
- package/dist/cli/tui/slash-commands.js +3 -28
- package/dist/cli/tui/tool-permission.d.ts +36 -0
- package/dist/cli/tui/tool-permission.js +386 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +28 -13
- package/dist/core/runtime.d.ts +50 -2
- package/dist/core/runtime.js +30 -3
- package/dist/extensions/claude-extension-tools.d.ts +3 -2
- package/dist/extensions/claude-extension-tools.js +15 -5
- package/dist/extensions/claude-extensions.d.ts +1 -0
- package/dist/extensions/claude-extensions.js +10 -0
- package/dist/hooks/claude-hook-tools.js +1 -1
- package/dist/mcp/claude-mcp-tools.js +12 -6
- package/dist/permissions/bash-ast.d.ts +39 -0
- package/dist/permissions/bash-ast.js +1046 -0
- package/dist/permissions/bash-normalization.d.ts +5 -0
- package/dist/permissions/bash-normalization.js +241 -0
- package/dist/permissions/bash-path-safety.d.ts +28 -0
- package/dist/permissions/bash-path-safety.js +575 -0
- package/dist/permissions/claude-permission-resolver.d.ts +7 -0
- package/dist/permissions/claude-permission-resolver.js +351 -40
- package/dist/permissions/claude-shell-permission.d.ts +4 -0
- package/dist/permissions/claude-shell-permission.js +108 -0
- package/dist/permissions/permission-updates.d.ts +16 -0
- package/dist/permissions/permission-updates.js +320 -0
- package/dist/permissions/sed-safety.d.ts +9 -0
- package/dist/permissions/sed-safety.js +172 -0
- package/dist/permissions/shell-rule-matching.d.ts +14 -0
- package/dist/permissions/shell-rule-matching.js +76 -0
- package/dist/tools/claude-interactive-tools.d.ts +10 -1
- package/dist/tools/claude-interactive-tools.js +10 -4
- package/dist/tools/local-tools.js +22 -2
- package/package.json +3 -1
package/dist/cli/interactive.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir, tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
5
7
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
8
|
import { Box, Text, render, useApp, useInput } from 'ink';
|
|
9
|
+
import { permissionRuleValueToString } from '../permissions/permission-updates.js';
|
|
7
10
|
import { AgentRunCancelledError } from '../core/runtime.js';
|
|
8
|
-
import { claudePermissionActionKey, } from '../permissions/claude-permission-resolver.js';
|
|
11
|
+
import { claudePermissionActionKey, claudePermissionRuleMatches, } from '../permissions/claude-permission-resolver.js';
|
|
9
12
|
import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
10
|
-
import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, PermissionDashboard, SelectionMenu, SessionPicker,
|
|
13
|
+
import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, PermissionDashboard, SelectionMenu, SessionPicker, ThemePicker, CustomThemeEditor, Transcript, WelcomePanel, useTerminalWidth, } from './tui/claude-style.js';
|
|
11
14
|
import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js';
|
|
12
15
|
import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
|
|
13
16
|
import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
|
|
14
17
|
import { createRecentlyDeniedStore, } from './tui/recently-denied.js';
|
|
15
18
|
import { filterTuiSlashCommands, mergeTuiSlashCommands, slashCommandQuery, } from './tui/slash-commands.js';
|
|
16
|
-
import { createComposerEditor, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursorByWord, } from './tui/composer-editor.js';
|
|
19
|
+
import { createComposerEditor, deleteComposerBackward, deleteComposerForward, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursor, moveComposerCursorByWord, } from './tui/composer-editor.js';
|
|
17
20
|
import { applyMentionReference, fileReferenceAtCursor, filterTuiMentionEntries, loadTuiFileEntries, } from './tui/file-picker.js';
|
|
18
21
|
import { editTuiPrompt, openTuiEditorFile, } from './tui/external-editor.js';
|
|
19
22
|
import { suspendTuiProcess } from './tui/terminal-suspend.js';
|
|
@@ -26,10 +29,13 @@ import { loadTuiThemeSettings, saveTuiThemeSettings, themeSettingsWithCustomThem
|
|
|
26
29
|
import { DEFAULT_TUI_THEME_SETTINGS, TUI_THEMES, TuiThemeProvider, tuiPalette, } from './tui/theme.js';
|
|
27
30
|
import { CUSTOM_THEME_TOKENS, createTuiCustomTheme, deleteTuiCustomTheme, loadTuiCustomThemes, updateTuiCustomTheme, } from './tui/custom-themes.js';
|
|
28
31
|
import { setupTuiTerminal, terminalSetupTuiSlashCommand, } from './tui/terminal-setup.js';
|
|
32
|
+
import { commitElicitationText, createTuiElicitationForm, elicitationFormIsValid, elicitationTextValue, expandElicitationOptions, focusedElicitationField, McpElicitationForm, McpElicitationUrl, moveElicitationFocus, moveElicitationOption, selectElicitationOption, toggleElicitationBoolean, typeaheadElicitationOption, unsetElicitationField, validateTuiElicitationForm, } from './tui/mcp-elicitation.js';
|
|
33
|
+
import { openTuiUrl } from './tui/open-url.js';
|
|
34
|
+
import { projectTuiToolPermission, ToolPermissionDialog, } from './tui/tool-permission.js';
|
|
29
35
|
import { ConfigDashboard, projectConfigRows } from './tui/config-dashboard.js';
|
|
30
36
|
import { loadConfigSettings, saveConfigSetting, configSettingDefinition, resolveConfigSettingsLocation, } from './tui/config-settings.js';
|
|
31
37
|
import { projectRuntimeSettings, loadRuntimeSettings, } from './tui/runtime-settings.js';
|
|
32
|
-
import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap,
|
|
38
|
+
import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap, spinnerTip, shouldShowCopyPicker, } from './tui/runtime-interactions.js';
|
|
33
39
|
import { notifyTerminal, } from './tui/terminal-notifications.js';
|
|
34
40
|
import { McpPanel } from './tui/mcp-panel.js';
|
|
35
41
|
import { McpPanelController, mcpRuntimeFromSession, } from './tui/mcp-panel-controller.js';
|
|
@@ -38,6 +44,17 @@ import { TaskPanel, initialTuiTaskPanelState, projectTuiTasks, reconcileTuiTaskP
|
|
|
38
44
|
import { ClaudeMcpManagement } from '../mcp/claude-mcp-management.js';
|
|
39
45
|
const EMPTY_SLASH_COMMANDS = [];
|
|
40
46
|
const EMPTY_AGENTS = [];
|
|
47
|
+
const estimateFileTokens = async (path) => {
|
|
48
|
+
try {
|
|
49
|
+
const content = await readFile(path, 'utf8');
|
|
50
|
+
if (!content.trim())
|
|
51
|
+
return 0;
|
|
52
|
+
return Math.max(1, Math.round(content.length / 4));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
41
58
|
const EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
42
59
|
const PERMISSION_OPTIONS = [
|
|
43
60
|
{
|
|
@@ -215,7 +232,7 @@ function ordinal(value) {
|
|
|
215
232
|
return `${value}th`;
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
|
-
export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, }) {
|
|
235
|
+
export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, }) {
|
|
219
236
|
const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
|
|
220
237
|
const width = useTerminalWidth(terminalWidth);
|
|
221
238
|
const keybindingsRoot = useMemo(() => resolve(keybindingsConfigRoot ??
|
|
@@ -271,6 +288,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
271
288
|
const [sessionId, setSessionId] = useState(resume?.sessionId ?? null);
|
|
272
289
|
const sessionIdRef = useRef(resume?.sessionId ?? null);
|
|
273
290
|
sessionIdRef.current = sessionId;
|
|
291
|
+
const [sessionName, setSessionName] = useState(null);
|
|
274
292
|
const [activeSessionSummary, setActiveSessionSummary] = useState(() => resume?.sessionId
|
|
275
293
|
? initialSessions.find((session) => session.sessionId === resume.sessionId)
|
|
276
294
|
: undefined);
|
|
@@ -354,14 +372,26 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
354
372
|
const runtimePreferencesRef = useRef(runtimePreferences);
|
|
355
373
|
const [permission, setPermission] = useState(null);
|
|
356
374
|
const permissionRef = useRef(null);
|
|
375
|
+
const [permissionSelection, setPermissionSelection] = useState(0);
|
|
376
|
+
const [permissionFeedbackMode, setPermissionFeedbackMode] = useState(false);
|
|
377
|
+
const [permissionRuleEditor, setPermissionRuleEditor] = useState(null);
|
|
378
|
+
const immediatePermissionRulesRef = useRef([]);
|
|
379
|
+
const toolPermissionModel = useMemo(() => permission?.kind === 'tool'
|
|
380
|
+
? projectTuiToolPermission(permission.call, runtimeCwd, sensitiveValues, permission.decision)
|
|
381
|
+
: null, [permission, runtimeCwd, sensitiveValues]);
|
|
357
382
|
const [elicitation, setElicitation] = useState(null);
|
|
358
383
|
const elicitationRef = useRef(null);
|
|
384
|
+
const [elicitationForm, setElicitationForm] = useState(null);
|
|
385
|
+
const [elicitationUrlWaiting, setElicitationUrlWaiting] = useState(false);
|
|
386
|
+
const elicitationUrlWaitingRef = useRef(null);
|
|
359
387
|
const [question, setQuestion] = useState(null);
|
|
360
388
|
const questionRef = useRef(null);
|
|
361
389
|
const questionTimeoutRef = useRef(null);
|
|
362
390
|
const recapShownRef = useRef(false);
|
|
363
391
|
const [planApproval, setPlanApproval] = useState(null);
|
|
364
392
|
const planApprovalRef = useRef(null);
|
|
393
|
+
const [planApprovalSelection, setPlanApprovalSelection] = useState(0);
|
|
394
|
+
const [planApprovalFeedbackMode, setPlanApprovalFeedbackMode] = useState(false);
|
|
365
395
|
const serviceRef = useRef(null);
|
|
366
396
|
const serviceCreationRef = useRef(undefined);
|
|
367
397
|
const onCleanupRef = useRef(onCleanup);
|
|
@@ -422,6 +452,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
422
452
|
permissionMode: runtimePreferences.permissionMode,
|
|
423
453
|
...(contextWindowTokens === undefined ? {} : { contextWindowTokens }),
|
|
424
454
|
};
|
|
455
|
+
const statusAuthSource = process.env.PRAXIS_API_KEY
|
|
456
|
+
? 'PRAXIS_API_KEY'
|
|
457
|
+
: process.env.ANTHROPIC_API_KEY
|
|
458
|
+
? 'ANTHROPIC_API_KEY'
|
|
459
|
+
: undefined;
|
|
460
|
+
const statusBaseUrl = process.env.PRAXIS_BASE_URL;
|
|
461
|
+
const statusProxy = process.env.HTTPS_PROXY ??
|
|
462
|
+
process.env.https_proxy ??
|
|
463
|
+
process.env.ALL_PROXY ??
|
|
464
|
+
process.env.HTTP_PROXY;
|
|
465
|
+
const statusSettingSources = (() => {
|
|
466
|
+
const projectSettings = existsSync(join(runtimeCwd, '.claude', 'settings.json')) ||
|
|
467
|
+
existsSync(join(runtimeCwd, 'settings.json'));
|
|
468
|
+
return projectSettings ? 'User settings, Project settings' : 'User settings';
|
|
469
|
+
})();
|
|
425
470
|
const workspaceDirectories = [
|
|
426
471
|
runtimeCwd,
|
|
427
472
|
...runtimePreferences.additionalDirectories.filter((path) => path !== runtimeCwd),
|
|
@@ -438,23 +483,46 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
438
483
|
]
|
|
439
484
|
: []),
|
|
440
485
|
], [allowDangerouslySkipPermissions]);
|
|
441
|
-
const modelOptions = useMemo(() =>
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
: '
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
486
|
+
const modelOptions = useMemo(() => {
|
|
487
|
+
const current = runtimePreferences.model;
|
|
488
|
+
const options = [
|
|
489
|
+
{
|
|
490
|
+
label: 'Default (recommended)',
|
|
491
|
+
description: `Use the invocation default (currently ${runtimeDisplay.model ?? 'provider default'})`,
|
|
492
|
+
selected: current === undefined,
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
if ((process.env.PRAXIS_PROVIDER ?? 'openai') === 'anthropic') {
|
|
496
|
+
options.push({
|
|
497
|
+
label: 'Opus',
|
|
498
|
+
model: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? 'opus',
|
|
499
|
+
description: 'Most capable for complex work',
|
|
500
|
+
}, {
|
|
501
|
+
label: 'Sonnet',
|
|
502
|
+
model: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'sonnet',
|
|
503
|
+
description: 'Best for everyday tasks',
|
|
504
|
+
}, {
|
|
505
|
+
label: 'Haiku',
|
|
506
|
+
model: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? 'haiku',
|
|
507
|
+
description: 'Fastest for quick answers',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
else if (current) {
|
|
511
|
+
options.push({
|
|
512
|
+
label: current,
|
|
513
|
+
model: current,
|
|
514
|
+
description: 'Current provider model',
|
|
515
|
+
selected: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
for (const option of options)
|
|
519
|
+
option.selected = option.model === current || (!option.model && !current);
|
|
520
|
+
options.push({
|
|
454
521
|
label: 'Enter a model ID…',
|
|
455
522
|
description: 'Use any model identifier supported by the configured provider.',
|
|
456
|
-
}
|
|
457
|
-
|
|
523
|
+
});
|
|
524
|
+
return options;
|
|
525
|
+
}, [runtimeDisplay.model, runtimePreferences.model]);
|
|
458
526
|
useEffect(() => {
|
|
459
527
|
setAvailableSlashCommands(slashCommands);
|
|
460
528
|
}, [slashCommands]);
|
|
@@ -493,7 +561,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
493
561
|
permissionRef.current?.resolve(false);
|
|
494
562
|
elicitationRef.current?.resolve({ action: 'cancel' });
|
|
495
563
|
questionRef.current?.resolve(null);
|
|
496
|
-
planApprovalRef.current?.resolve(
|
|
564
|
+
planApprovalRef.current?.resolve({ behavior: 'deny' });
|
|
497
565
|
exit();
|
|
498
566
|
};
|
|
499
567
|
if (signal.aborted)
|
|
@@ -507,7 +575,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
507
575
|
permissionRef.current?.resolve(false);
|
|
508
576
|
elicitationRef.current?.resolve({ action: 'cancel' });
|
|
509
577
|
questionRef.current?.resolve(null);
|
|
510
|
-
planApprovalRef.current?.resolve(
|
|
578
|
+
planApprovalRef.current?.resolve({ behavior: 'deny' });
|
|
511
579
|
if (exitConfirmationTimerRef.current)
|
|
512
580
|
clearTimeout(exitConfirmationTimerRef.current);
|
|
513
581
|
if (btwCopiedTimerRef.current)
|
|
@@ -898,7 +966,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
898
966
|
permissionRef.current?.resolve(false);
|
|
899
967
|
elicitationRef.current?.resolve({ action: 'cancel' });
|
|
900
968
|
questionRef.current?.resolve(null);
|
|
901
|
-
planApprovalRef.current?.resolve(
|
|
969
|
+
planApprovalRef.current?.resolve({ behavior: 'deny' });
|
|
902
970
|
onCancel?.();
|
|
903
971
|
exit();
|
|
904
972
|
return;
|
|
@@ -1047,6 +1115,16 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1047
1115
|
kind: 'notice',
|
|
1048
1116
|
text: `MCP elicitation completed · ${event.mcpServerName}`,
|
|
1049
1117
|
});
|
|
1118
|
+
if (elicitationUrlWaitingRef.current?.request.serverName ===
|
|
1119
|
+
event.mcpServerName &&
|
|
1120
|
+
elicitationUrlWaitingRef.current.request.elicitationId ===
|
|
1121
|
+
event.elicitationId) {
|
|
1122
|
+
elicitationUrlWaitingRef.current = null;
|
|
1123
|
+
setElicitationUrlWaiting(false);
|
|
1124
|
+
setElicitation(null);
|
|
1125
|
+
setElicitationForm(null);
|
|
1126
|
+
clearComposerInput();
|
|
1127
|
+
}
|
|
1050
1128
|
break;
|
|
1051
1129
|
case 'tool-use-summary':
|
|
1052
1130
|
append({ kind: 'notice', text: event.summary });
|
|
@@ -1076,11 +1154,16 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1076
1154
|
}
|
|
1077
1155
|
}
|
|
1078
1156
|
};
|
|
1079
|
-
const requestApproval = (call, kind) => new Promise((resolveApproval) => {
|
|
1157
|
+
const requestApproval = (call, kind, decision) => new Promise((resolveApproval) => {
|
|
1158
|
+
const projected = kind === 'tool'
|
|
1159
|
+
? projectTuiToolPermission(call, runtimeCwdRef.current, sensitiveValues, decision)
|
|
1160
|
+
: null;
|
|
1161
|
+
const editableRule = projected?.options.find((option) => option.editableRule)?.editableRule;
|
|
1080
1162
|
let settled = false;
|
|
1081
1163
|
const pending = {
|
|
1082
1164
|
kind,
|
|
1083
1165
|
call,
|
|
1166
|
+
...(decision ? { decision } : {}),
|
|
1084
1167
|
resolve: (approved) => {
|
|
1085
1168
|
if (settled)
|
|
1086
1169
|
return;
|
|
@@ -1091,28 +1174,44 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1091
1174
|
resolveApproval(approved);
|
|
1092
1175
|
},
|
|
1093
1176
|
};
|
|
1177
|
+
clearComposerInput();
|
|
1178
|
+
setPermissionSelection(0);
|
|
1179
|
+
setPermissionFeedbackMode(false);
|
|
1180
|
+
setPermissionRuleEditor(editableRule ? createComposerEditor(editableRule.initialValue) : null);
|
|
1094
1181
|
permissionRef.current = pending;
|
|
1095
1182
|
setPermission(pending);
|
|
1096
1183
|
});
|
|
1097
|
-
const approveTool = (call) => requestApproval(call, 'tool');
|
|
1184
|
+
const approveTool = (call, _originalCall, decision) => requestApproval(call, 'tool', decision);
|
|
1098
1185
|
const requestElicitation = (request) => new Promise((resolveResult) => {
|
|
1099
1186
|
let settled = false;
|
|
1100
1187
|
const pending = {
|
|
1101
1188
|
request,
|
|
1102
|
-
resolve: (result) => {
|
|
1189
|
+
resolve: (result, options) => {
|
|
1103
1190
|
if (settled)
|
|
1104
1191
|
return;
|
|
1105
1192
|
settled = true;
|
|
1106
1193
|
if (elicitationRef.current === pending)
|
|
1107
1194
|
elicitationRef.current = null;
|
|
1108
|
-
|
|
1195
|
+
if (!options?.keepUrlDialog) {
|
|
1196
|
+
setElicitation((current) => (current === pending ? null : current));
|
|
1197
|
+
setElicitationForm(null);
|
|
1198
|
+
clearComposerInput();
|
|
1199
|
+
}
|
|
1109
1200
|
resolveResult(result);
|
|
1110
1201
|
},
|
|
1111
1202
|
};
|
|
1203
|
+
const form = createTuiElicitationForm(request.requestedSchema);
|
|
1204
|
+
setElicitationForm(form);
|
|
1205
|
+
setElicitationUrlWaiting(false);
|
|
1206
|
+
elicitationUrlWaitingRef.current = null;
|
|
1207
|
+
updateComposerInput(elicitationTextValue(form));
|
|
1112
1208
|
elicitationRef.current = pending;
|
|
1113
1209
|
setElicitation(pending);
|
|
1114
1210
|
});
|
|
1115
|
-
const approveRecovery = (call) => resume?.retryInterruptedTools
|
|
1211
|
+
const approveRecovery = (call) => resume?.retryInterruptedTools
|
|
1212
|
+
? true
|
|
1213
|
+
: requestApproval(call, 'recovery').then((approval) => approval === true ||
|
|
1214
|
+
(typeof approval === 'object' && approval.behavior === 'allow'));
|
|
1116
1215
|
const askUser = (questions, signal) => new Promise((resolveResult) => {
|
|
1117
1216
|
let settled = false;
|
|
1118
1217
|
const abort = () => pending.resolve(null);
|
|
@@ -1153,7 +1252,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1153
1252
|
});
|
|
1154
1253
|
const approvePlan = (request, signal) => new Promise((resolveApproval) => {
|
|
1155
1254
|
let settled = false;
|
|
1156
|
-
const abort = () => pending.resolve(
|
|
1255
|
+
const abort = () => pending.resolve({ behavior: 'deny' });
|
|
1157
1256
|
const pending = {
|
|
1158
1257
|
request,
|
|
1159
1258
|
resolve: (approved) => {
|
|
@@ -1168,10 +1267,13 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1168
1267
|
},
|
|
1169
1268
|
};
|
|
1170
1269
|
if (signal?.aborted) {
|
|
1171
|
-
pending.resolve(
|
|
1270
|
+
pending.resolve({ behavior: 'deny' });
|
|
1172
1271
|
return;
|
|
1173
1272
|
}
|
|
1174
1273
|
signal?.addEventListener('abort', abort, { once: true });
|
|
1274
|
+
clearComposerInput();
|
|
1275
|
+
setPlanApprovalSelection(0);
|
|
1276
|
+
setPlanApprovalFeedbackMode(false);
|
|
1175
1277
|
planApprovalRef.current = pending;
|
|
1176
1278
|
setPlanApproval(pending);
|
|
1177
1279
|
});
|
|
@@ -1203,6 +1305,8 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1203
1305
|
return next;
|
|
1204
1306
|
};
|
|
1205
1307
|
const isSessionActionApproved = (call) => {
|
|
1308
|
+
if (immediatePermissionRulesRef.current.some((rule) => claudePermissionRuleMatches(rule, call, runtimeCwdRef.current)))
|
|
1309
|
+
return true;
|
|
1206
1310
|
const activeSessionId = sessionIdRef.current;
|
|
1207
1311
|
return activeSessionId
|
|
1208
1312
|
? (approvedSessionActionsRef.current
|
|
@@ -1424,6 +1528,39 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1424
1528
|
onTurnChange?.(loading);
|
|
1425
1529
|
void loading.finally(() => onTurnChange?.(null));
|
|
1426
1530
|
};
|
|
1531
|
+
const writeCopyCandidate = async (candidate) => {
|
|
1532
|
+
const directory = join(tmpdir(), 'claude');
|
|
1533
|
+
await mkdir(directory, { recursive: true });
|
|
1534
|
+
const path = join(directory, candidate.filename);
|
|
1535
|
+
await writeFile(path, candidate.text, 'utf8');
|
|
1536
|
+
return path;
|
|
1537
|
+
};
|
|
1538
|
+
const copyCandidate = (candidate, savePreference = false) => {
|
|
1539
|
+
const copying = (async () => {
|
|
1540
|
+
try {
|
|
1541
|
+
if (savePreference) {
|
|
1542
|
+
const snapshot = await saveConfigSetting('copyFullResponse', true, configTarget);
|
|
1543
|
+
await reloadRuntimeSettings(snapshot);
|
|
1544
|
+
}
|
|
1545
|
+
await clipboardWriter(candidate.text);
|
|
1546
|
+
let result = `Copied to clipboard (${candidate.text.length} characters, ${candidate.text.split('\n').length} lines)`;
|
|
1547
|
+
try {
|
|
1548
|
+
result += `\nAlso written to ${await writeCopyCandidate(candidate)}`;
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
// Clipboard success is authoritative; the temp file is a fallback.
|
|
1552
|
+
}
|
|
1553
|
+
if (savePreference)
|
|
1554
|
+
result += '\nPreference saved. Use /config to change copyFullResponse';
|
|
1555
|
+
append({ kind: 'local-result', text: result });
|
|
1556
|
+
}
|
|
1557
|
+
catch (error) {
|
|
1558
|
+
warn(error);
|
|
1559
|
+
}
|
|
1560
|
+
})();
|
|
1561
|
+
onTurnChange?.(copying);
|
|
1562
|
+
void copying.finally(() => onTurnChange?.(null));
|
|
1563
|
+
};
|
|
1427
1564
|
const copyResponse = (position) => {
|
|
1428
1565
|
const response = [...history]
|
|
1429
1566
|
.reverse()
|
|
@@ -1435,21 +1572,48 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1435
1572
|
});
|
|
1436
1573
|
return;
|
|
1437
1574
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1575
|
+
const candidates = copyCandidates(response.text);
|
|
1576
|
+
const full = candidates[0];
|
|
1577
|
+
if (!full)
|
|
1578
|
+
return;
|
|
1579
|
+
if (runtimeSettingsRef.current.copyFullResponse ||
|
|
1580
|
+
!shouldShowCopyPicker(response.text)) {
|
|
1581
|
+
copyCandidate(full);
|
|
1445
1582
|
return;
|
|
1446
1583
|
}
|
|
1447
|
-
|
|
1448
|
-
kind: '
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1584
|
+
updateMenu({
|
|
1585
|
+
kind: 'copy',
|
|
1586
|
+
candidates,
|
|
1587
|
+
selectedIndex: 0,
|
|
1588
|
+
messageAge: position - 1,
|
|
1589
|
+
});
|
|
1590
|
+
};
|
|
1591
|
+
const openSettings = (tab) => {
|
|
1592
|
+
const initial = {
|
|
1593
|
+
kind: 'config',
|
|
1594
|
+
snapshot: { settings: {}, state: {} },
|
|
1595
|
+
tab,
|
|
1596
|
+
selectedIndex: 0,
|
|
1597
|
+
query: '',
|
|
1598
|
+
searchFocused: tab === 'config',
|
|
1599
|
+
};
|
|
1600
|
+
updateMenu(initial);
|
|
1601
|
+
const loading = (async () => {
|
|
1602
|
+
setBusy(true);
|
|
1603
|
+
try {
|
|
1604
|
+
const snapshot = await loadConfigSettings(configTarget);
|
|
1605
|
+
if (menuRef.current?.kind === 'config')
|
|
1606
|
+
updateMenu({ ...menuRef.current, snapshot });
|
|
1607
|
+
}
|
|
1608
|
+
catch (error) {
|
|
1609
|
+
warn(error);
|
|
1610
|
+
}
|
|
1611
|
+
finally {
|
|
1612
|
+
setBusy(false);
|
|
1613
|
+
}
|
|
1614
|
+
})();
|
|
1615
|
+
onTurnChange?.(loading);
|
|
1616
|
+
void loading.finally(() => onTurnChange?.(null));
|
|
1453
1617
|
};
|
|
1454
1618
|
const exportConversation = (method) => {
|
|
1455
1619
|
if (method === 'file') {
|
|
@@ -1808,6 +1972,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1808
1972
|
if (!name)
|
|
1809
1973
|
throw new Error('Could not generate a session name');
|
|
1810
1974
|
await commands.rename(sessionId, name);
|
|
1975
|
+
setSessionName(name);
|
|
1811
1976
|
append({ kind: 'local-result', text: `Session renamed to: ${name}` });
|
|
1812
1977
|
}
|
|
1813
1978
|
catch (error) {
|
|
@@ -2381,20 +2546,270 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2381
2546
|
}
|
|
2382
2547
|
}
|
|
2383
2548
|
if (permission) {
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2549
|
+
const options = permission.kind === 'tool' && toolPermissionModel
|
|
2550
|
+
? toolPermissionModel.options
|
|
2551
|
+
: [
|
|
2552
|
+
{ action: 'allow-once', label: 'Yes' },
|
|
2553
|
+
{ action: 'deny', label: 'No' },
|
|
2554
|
+
];
|
|
2555
|
+
const optionCount = options.length;
|
|
2556
|
+
const resolvePermission = (selectedIndex) => {
|
|
2557
|
+
const feedback = inputRef.current.trim();
|
|
2558
|
+
clearComposerInput();
|
|
2559
|
+
setPermissionFeedbackMode(false);
|
|
2560
|
+
const selected = options[selectedIndex];
|
|
2561
|
+
if (!selected)
|
|
2562
|
+
return;
|
|
2563
|
+
if (selected.action === 'allow-once') {
|
|
2564
|
+
permission.resolve(feedback ? { behavior: 'allow', feedback } : { behavior: 'allow' });
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (permission.kind === 'tool' &&
|
|
2568
|
+
selected.action === 'persist-rule' &&
|
|
2569
|
+
(selected.rule || selected.editableRule || selected.updates?.length)) {
|
|
2570
|
+
const editedRule = permissionRuleEditor?.text.trim();
|
|
2571
|
+
const rule = selected.editableRule
|
|
2572
|
+
? editedRule
|
|
2573
|
+
? `${selected.editableRule.toolName}(${editedRule})`
|
|
2574
|
+
: undefined
|
|
2575
|
+
: selected.rule;
|
|
2576
|
+
const updates = selected.editableRule
|
|
2577
|
+
? rule
|
|
2578
|
+
? [
|
|
2579
|
+
{
|
|
2580
|
+
type: 'addRules',
|
|
2581
|
+
rules: [
|
|
2582
|
+
{
|
|
2583
|
+
toolName: selected.editableRule.toolName,
|
|
2584
|
+
ruleContent: editedRule ?? '',
|
|
2585
|
+
},
|
|
2586
|
+
],
|
|
2587
|
+
behavior: 'allow',
|
|
2588
|
+
destination: 'localSettings',
|
|
2589
|
+
},
|
|
2590
|
+
]
|
|
2591
|
+
: []
|
|
2592
|
+
: (selected.updates ??
|
|
2593
|
+
(rule
|
|
2594
|
+
? [
|
|
2595
|
+
{
|
|
2596
|
+
type: 'addRules',
|
|
2597
|
+
rules: [
|
|
2598
|
+
/^([A-Za-z][\w-]*)(?:\((.*)\))?$/u.exec(rule),
|
|
2599
|
+
].flatMap((match) => match?.[1]
|
|
2600
|
+
? [
|
|
2601
|
+
{
|
|
2602
|
+
toolName: match[1],
|
|
2603
|
+
...(match[2] === undefined
|
|
2604
|
+
? {}
|
|
2605
|
+
: { ruleContent: match[2] }),
|
|
2606
|
+
},
|
|
2607
|
+
]
|
|
2608
|
+
: []),
|
|
2609
|
+
behavior: 'allow',
|
|
2610
|
+
destination: 'localSettings',
|
|
2611
|
+
},
|
|
2612
|
+
]
|
|
2613
|
+
: []));
|
|
2614
|
+
if (!rule && updates.length === 0) {
|
|
2615
|
+
permission.resolve({ behavior: 'allow' });
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const saving = (async () => {
|
|
2619
|
+
try {
|
|
2620
|
+
for (const update of updates) {
|
|
2621
|
+
if (update.type !== 'addRules' ||
|
|
2622
|
+
update.destination !== 'localSettings') {
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
for (const value of update.rules) {
|
|
2626
|
+
const savedRule = permissionRuleValueToString(value);
|
|
2627
|
+
await permissionStore.add({
|
|
2628
|
+
behavior: update.behavior,
|
|
2629
|
+
rule: savedRule,
|
|
2630
|
+
scope: 'local',
|
|
2631
|
+
});
|
|
2632
|
+
immediatePermissionRulesRef.current.push(savedRule);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
permission.resolve({
|
|
2636
|
+
behavior: 'allow',
|
|
2637
|
+
updatedPermissions: updates,
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
catch (error) {
|
|
2641
|
+
warn(error);
|
|
2642
|
+
}
|
|
2643
|
+
})();
|
|
2644
|
+
onTurnChange?.(saving);
|
|
2645
|
+
void saving.finally(() => onTurnChange?.(null));
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
if (permission.kind === 'tool' &&
|
|
2649
|
+
selected.action === 'allow-session-action' &&
|
|
2650
|
+
(selected.rule || selected.updates?.length)) {
|
|
2651
|
+
if (selected.rule)
|
|
2652
|
+
immediatePermissionRulesRef.current.push(selected.rule);
|
|
2653
|
+
permission.resolve({
|
|
2654
|
+
behavior: 'allow',
|
|
2655
|
+
updatedPermissions: selected.updates ?? [],
|
|
2656
|
+
});
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
if (permission.kind === 'tool' &&
|
|
2660
|
+
selected.action === 'allow-session-edits') {
|
|
2661
|
+
for (const rule of ['Write', 'Edit', 'NotebookEdit']) {
|
|
2662
|
+
if (!immediatePermissionRulesRef.current.includes(rule))
|
|
2663
|
+
immediatePermissionRulesRef.current.push(rule);
|
|
2664
|
+
}
|
|
2665
|
+
updateRuntimePreferences((current) => ({
|
|
2666
|
+
...current,
|
|
2667
|
+
permissionMode: 'acceptEdits',
|
|
2668
|
+
}));
|
|
2669
|
+
permission.resolve({
|
|
2670
|
+
behavior: 'allow',
|
|
2671
|
+
updatedPermissions: selected.updates?.length
|
|
2672
|
+
? selected.updates
|
|
2673
|
+
: [
|
|
2674
|
+
{
|
|
2675
|
+
type: 'setMode',
|
|
2676
|
+
mode: 'acceptEdits',
|
|
2677
|
+
destination: 'session',
|
|
2678
|
+
},
|
|
2679
|
+
],
|
|
2680
|
+
});
|
|
2681
|
+
const activeSessionId = sessionIdRef.current;
|
|
2682
|
+
if (activeSessionId && serviceRef.current?.setPermissionMode) {
|
|
2683
|
+
const saving = serviceRef.current
|
|
2684
|
+
.setPermissionMode(activeSessionId, 'acceptEdits')
|
|
2685
|
+
.catch((error) => warn(error));
|
|
2686
|
+
onTurnChange?.(saving);
|
|
2687
|
+
void saving.finally(() => onTurnChange?.(null));
|
|
2688
|
+
}
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
permission.resolve(feedback ? { behavior: 'deny', message: feedback } : false);
|
|
2692
|
+
};
|
|
2693
|
+
if (key.escape) {
|
|
2388
2694
|
permission.resolve(false);
|
|
2389
2695
|
}
|
|
2696
|
+
else if (permissionFeedbackMode) {
|
|
2697
|
+
if (key.tab) {
|
|
2698
|
+
clearComposerInput();
|
|
2699
|
+
setPermissionFeedbackMode(false);
|
|
2700
|
+
}
|
|
2701
|
+
else if (key.return) {
|
|
2702
|
+
resolvePermission(permissionSelection);
|
|
2703
|
+
}
|
|
2704
|
+
else {
|
|
2705
|
+
editComposer();
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
else if (key.upArrow) {
|
|
2709
|
+
setPermissionSelection((current) => current === 0 ? optionCount - 1 : current - 1);
|
|
2710
|
+
}
|
|
2711
|
+
else if (key.downArrow) {
|
|
2712
|
+
setPermissionSelection((current) => (current + 1) % optionCount);
|
|
2713
|
+
}
|
|
2714
|
+
else if (key.tab &&
|
|
2715
|
+
(options[permissionSelection]?.action === 'allow-once' ||
|
|
2716
|
+
options[permissionSelection]?.action === 'deny')) {
|
|
2717
|
+
clearComposerInput();
|
|
2718
|
+
setPermissionFeedbackMode(true);
|
|
2719
|
+
}
|
|
2720
|
+
else if (options[permissionSelection]?.editableRule && !key.return) {
|
|
2721
|
+
if (key.leftArrow) {
|
|
2722
|
+
setPermissionRuleEditor((current) => moveComposerCursor(current ?? createComposerEditor(), -1));
|
|
2723
|
+
}
|
|
2724
|
+
else if (key.rightArrow) {
|
|
2725
|
+
setPermissionRuleEditor((current) => moveComposerCursor(current ?? createComposerEditor(), 1));
|
|
2726
|
+
}
|
|
2727
|
+
else if (key.backspace) {
|
|
2728
|
+
setPermissionRuleEditor((current) => deleteComposerBackward(current ?? createComposerEditor()));
|
|
2729
|
+
}
|
|
2730
|
+
else if (key.delete) {
|
|
2731
|
+
setPermissionRuleEditor((current) => deleteComposerForward(current ?? createComposerEditor()));
|
|
2732
|
+
}
|
|
2733
|
+
else if (value && !key.ctrl && !key.meta) {
|
|
2734
|
+
setPermissionRuleEditor((current) => insertComposerText(current ?? createComposerEditor(), value));
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
else if (lower === 'y') {
|
|
2738
|
+
resolvePermission(0);
|
|
2739
|
+
}
|
|
2740
|
+
else if (lower === 'n') {
|
|
2741
|
+
resolvePermission(optionCount - 1);
|
|
2742
|
+
}
|
|
2743
|
+
else if (/^[1-9]$/u.test(value)) {
|
|
2744
|
+
const selectedIndex = Number(value) - 1;
|
|
2745
|
+
if (selectedIndex < optionCount)
|
|
2746
|
+
resolvePermission(selectedIndex);
|
|
2747
|
+
}
|
|
2748
|
+
else if (key.return) {
|
|
2749
|
+
resolvePermission(permissionSelection);
|
|
2750
|
+
}
|
|
2390
2751
|
return;
|
|
2391
2752
|
}
|
|
2392
2753
|
if (planApproval) {
|
|
2393
|
-
|
|
2394
|
-
|
|
2754
|
+
const elevatedMode = runtimeSettingsRef.current
|
|
2755
|
+
.useAutoModeDuringPlan
|
|
2756
|
+
? 'auto'
|
|
2757
|
+
: allowDangerouslySkipPermissions
|
|
2758
|
+
? 'bypassPermissions'
|
|
2759
|
+
: 'acceptEdits';
|
|
2760
|
+
const resolvePlanApproval = (selectedIndex) => {
|
|
2761
|
+
const feedback = inputRef.current.trim() || undefined;
|
|
2762
|
+
clearComposerInput();
|
|
2763
|
+
setPlanApprovalFeedbackMode(false);
|
|
2764
|
+
if (selectedIndex === 2) {
|
|
2765
|
+
planApproval.resolve({
|
|
2766
|
+
behavior: 'deny',
|
|
2767
|
+
...(feedback ? { feedback } : {}),
|
|
2768
|
+
});
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
planApproval.resolve({
|
|
2772
|
+
behavior: 'allow',
|
|
2773
|
+
permissionMode: selectedIndex === 0 ? elevatedMode : 'default',
|
|
2774
|
+
...(feedback ? { feedback } : {}),
|
|
2775
|
+
});
|
|
2776
|
+
};
|
|
2777
|
+
if (key.escape) {
|
|
2778
|
+
planApproval.resolve({ behavior: 'deny' });
|
|
2779
|
+
}
|
|
2780
|
+
else if (planApprovalFeedbackMode) {
|
|
2781
|
+
if (key.tab) {
|
|
2782
|
+
clearComposerInput();
|
|
2783
|
+
setPlanApprovalFeedbackMode(false);
|
|
2784
|
+
}
|
|
2785
|
+
else if (key.return) {
|
|
2786
|
+
resolvePlanApproval(planApprovalSelection);
|
|
2787
|
+
}
|
|
2788
|
+
else {
|
|
2789
|
+
editComposer();
|
|
2790
|
+
}
|
|
2395
2791
|
}
|
|
2396
|
-
else if (
|
|
2397
|
-
|
|
2792
|
+
else if (key.upArrow) {
|
|
2793
|
+
setPlanApprovalSelection((current) => (current === 0 ? 2 : current - 1));
|
|
2794
|
+
}
|
|
2795
|
+
else if (key.downArrow) {
|
|
2796
|
+
setPlanApprovalSelection((current) => (current + 1) % 3);
|
|
2797
|
+
}
|
|
2798
|
+
else if (key.tab) {
|
|
2799
|
+
clearComposerInput();
|
|
2800
|
+
setPlanApprovalFeedbackMode(true);
|
|
2801
|
+
}
|
|
2802
|
+
else if (lower === 'y') {
|
|
2803
|
+
resolvePlanApproval(0);
|
|
2804
|
+
}
|
|
2805
|
+
else if (lower === 'n') {
|
|
2806
|
+
resolvePlanApproval(2);
|
|
2807
|
+
}
|
|
2808
|
+
else if (/^[1-3]$/u.test(value)) {
|
|
2809
|
+
resolvePlanApproval(Number(value) - 1);
|
|
2810
|
+
}
|
|
2811
|
+
else if (key.return) {
|
|
2812
|
+
resolvePlanApproval(planApprovalSelection);
|
|
2398
2813
|
}
|
|
2399
2814
|
return;
|
|
2400
2815
|
}
|
|
@@ -2432,43 +2847,151 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2432
2847
|
return;
|
|
2433
2848
|
}
|
|
2434
2849
|
if (elicitation) {
|
|
2435
|
-
|
|
2436
|
-
elicitation.
|
|
2850
|
+
const form = elicitationForm ??
|
|
2851
|
+
createTuiElicitationForm(elicitation.request.requestedSchema);
|
|
2852
|
+
const currentField = focusedElicitationField(form);
|
|
2853
|
+
const textField = currentField &&
|
|
2854
|
+
['text', 'number', 'integer'].includes(currentField.kind);
|
|
2855
|
+
const commitCurrent = () => textField ? commitElicitationText(form, inputRef.current) : form;
|
|
2856
|
+
const showForm = (next) => {
|
|
2857
|
+
setElicitationForm(next);
|
|
2858
|
+
updateComposerInput(elicitationTextValue(next));
|
|
2859
|
+
};
|
|
2860
|
+
const move = (direction) => showForm(moveElicitationFocus(commitCurrent(), direction));
|
|
2861
|
+
if (elicitation.request.mode === 'url') {
|
|
2862
|
+
const openUrl = () => {
|
|
2863
|
+
void Promise.resolve()
|
|
2864
|
+
.then(() => elicitationUrlOpener(elicitation.request.url ?? ''))
|
|
2865
|
+
.catch(() => undefined);
|
|
2866
|
+
};
|
|
2867
|
+
const dismissUrlDialog = () => {
|
|
2868
|
+
elicitationUrlWaitingRef.current = null;
|
|
2869
|
+
setElicitationUrlWaiting(false);
|
|
2870
|
+
setElicitation(null);
|
|
2871
|
+
setElicitationForm(null);
|
|
2872
|
+
clearComposerInput();
|
|
2873
|
+
};
|
|
2874
|
+
if (key.escape) {
|
|
2875
|
+
if (elicitationUrlWaiting)
|
|
2876
|
+
dismissUrlDialog();
|
|
2877
|
+
else
|
|
2878
|
+
elicitation.resolve({ action: 'cancel' });
|
|
2879
|
+
}
|
|
2880
|
+
else if (key.leftArrow || key.rightArrow) {
|
|
2881
|
+
setElicitationForm({
|
|
2882
|
+
...form,
|
|
2883
|
+
focusIndex: form.focusIndex === 0 ? 1 : 0,
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
else if (key.return && form.focusIndex === 0) {
|
|
2887
|
+
if (elicitationUrlWaiting) {
|
|
2888
|
+
openUrl();
|
|
2889
|
+
}
|
|
2890
|
+
else {
|
|
2891
|
+
openUrl();
|
|
2892
|
+
elicitationUrlWaitingRef.current = elicitation;
|
|
2893
|
+
setElicitationUrlWaiting(true);
|
|
2894
|
+
elicitation.resolve({ action: 'accept' }, { keepUrlDialog: true });
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
else if (key.return && form.focusIndex === 1) {
|
|
2898
|
+
if (elicitationUrlWaiting)
|
|
2899
|
+
dismissUrlDialog();
|
|
2900
|
+
else
|
|
2901
|
+
elicitation.resolve({ action: 'decline' });
|
|
2902
|
+
}
|
|
2903
|
+
return;
|
|
2437
2904
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2905
|
+
if (form.expandedField && currentField) {
|
|
2906
|
+
if (key.escape || key.leftArrow) {
|
|
2907
|
+
setElicitationForm({ ...form, expandedField: undefined });
|
|
2908
|
+
}
|
|
2909
|
+
else if (key.upArrow) {
|
|
2910
|
+
showForm(moveElicitationOption(form, -1));
|
|
2911
|
+
}
|
|
2912
|
+
else if (key.downArrow) {
|
|
2913
|
+
showForm(moveElicitationOption(form, 1));
|
|
2443
2914
|
}
|
|
2444
|
-
else if (
|
|
2445
|
-
|
|
2915
|
+
else if (value === ' ') {
|
|
2916
|
+
setElicitationForm(selectElicitationOption(form, currentField.kind === 'enum'));
|
|
2446
2917
|
}
|
|
2447
|
-
else if (
|
|
2448
|
-
|
|
2918
|
+
else if (key.return) {
|
|
2919
|
+
showForm(moveElicitationFocus(selectElicitationOption(form, true, true), 1));
|
|
2920
|
+
}
|
|
2921
|
+
else if (printable && value) {
|
|
2922
|
+
setElicitationForm(typeaheadElicitationOption(form, value));
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
else if (key.escape) {
|
|
2926
|
+
elicitation.resolve({ action: 'cancel' });
|
|
2927
|
+
}
|
|
2928
|
+
else if (key.upArrow) {
|
|
2929
|
+
move(-1);
|
|
2930
|
+
}
|
|
2931
|
+
else if (key.downArrow) {
|
|
2932
|
+
move(1);
|
|
2933
|
+
}
|
|
2934
|
+
else if (!currentField && (key.leftArrow || key.rightArrow)) {
|
|
2935
|
+
setElicitationForm({
|
|
2936
|
+
...form,
|
|
2937
|
+
focusIndex: form.focusIndex === form.fields.length
|
|
2938
|
+
? form.fields.length + 1
|
|
2939
|
+
: form.fields.length,
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
else if (form.focusIndex === form.fields.length && key.return) {
|
|
2943
|
+
const validated = validateTuiElicitationForm(form);
|
|
2944
|
+
if (elicitationFormIsValid(validated)) {
|
|
2945
|
+
elicitation.resolve({
|
|
2946
|
+
action: 'accept',
|
|
2947
|
+
...(Object.keys(validated.values).length > 0
|
|
2948
|
+
? { content: { ...validated.values } }
|
|
2949
|
+
: {}),
|
|
2950
|
+
});
|
|
2449
2951
|
}
|
|
2450
2952
|
else {
|
|
2451
|
-
|
|
2452
|
-
const content = JSON.parse(answer);
|
|
2453
|
-
if (!content ||
|
|
2454
|
-
typeof content !== 'object' ||
|
|
2455
|
-
Array.isArray(content))
|
|
2456
|
-
throw new Error('elicitation content must be a JSON object');
|
|
2457
|
-
elicitation.resolve({
|
|
2458
|
-
action: 'accept',
|
|
2459
|
-
content: content,
|
|
2460
|
-
});
|
|
2461
|
-
}
|
|
2462
|
-
catch {
|
|
2463
|
-
append({
|
|
2464
|
-
kind: 'warning',
|
|
2465
|
-
text: 'Elicitation response must be accept, decline, cancel, or a JSON object.',
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2953
|
+
showForm(validated);
|
|
2468
2954
|
}
|
|
2469
2955
|
}
|
|
2470
|
-
else {
|
|
2471
|
-
|
|
2956
|
+
else if (form.focusIndex === form.fields.length + 1 && key.return) {
|
|
2957
|
+
elicitation.resolve({ action: 'decline' });
|
|
2958
|
+
}
|
|
2959
|
+
else if (currentField?.kind === 'boolean') {
|
|
2960
|
+
if (value === ' ' || lower === 'y' || lower === 'n') {
|
|
2961
|
+
let next = toggleElicitationBoolean(form);
|
|
2962
|
+
if (lower === 'n' && next.values[currentField.name] === true)
|
|
2963
|
+
next = toggleElicitationBoolean(next);
|
|
2964
|
+
if (lower === 'y' && next.values[currentField.name] === false)
|
|
2965
|
+
next = toggleElicitationBoolean(next);
|
|
2966
|
+
setElicitationForm(next);
|
|
2967
|
+
}
|
|
2968
|
+
else if (key.backspace) {
|
|
2969
|
+
setElicitationForm(unsetElicitationField(form));
|
|
2970
|
+
}
|
|
2971
|
+
else if (key.return) {
|
|
2972
|
+
move(1);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
else if (currentField &&
|
|
2976
|
+
['enum', 'multi-enum'].includes(currentField.kind)) {
|
|
2977
|
+
if (key.rightArrow) {
|
|
2978
|
+
setElicitationForm(expandElicitationOptions(form));
|
|
2979
|
+
}
|
|
2980
|
+
else if (key.backspace) {
|
|
2981
|
+
setElicitationForm(unsetElicitationField(form));
|
|
2982
|
+
}
|
|
2983
|
+
else if (key.return) {
|
|
2984
|
+
move(1);
|
|
2985
|
+
}
|
|
2986
|
+
else if (printable && value) {
|
|
2987
|
+
setElicitationForm(typeaheadElicitationOption(form, value));
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
else if (textField) {
|
|
2991
|
+
if (key.return)
|
|
2992
|
+
move(1);
|
|
2993
|
+
else
|
|
2994
|
+
editComposer();
|
|
2472
2995
|
}
|
|
2473
2996
|
return;
|
|
2474
2997
|
}
|
|
@@ -2522,41 +3045,6 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2522
3045
|
return;
|
|
2523
3046
|
}
|
|
2524
3047
|
const earlyMenu = menuRef.current;
|
|
2525
|
-
if (earlyMenu?.kind === 'copy') {
|
|
2526
|
-
if (key.escape || value === '\u001B') {
|
|
2527
|
-
updateMenu(null);
|
|
2528
|
-
return;
|
|
2529
|
-
}
|
|
2530
|
-
if (key.upArrow || key.downArrow) {
|
|
2531
|
-
updateMenu({
|
|
2532
|
-
...earlyMenu,
|
|
2533
|
-
selectedIndex: Math.max(0, Math.min(earlyMenu.candidates.length - 1, earlyMenu.selectedIndex + (key.upArrow ? -1 : 1))),
|
|
2534
|
-
});
|
|
2535
|
-
return;
|
|
2536
|
-
}
|
|
2537
|
-
if (/^[1-9]$/u.test(value)) {
|
|
2538
|
-
updateMenu({
|
|
2539
|
-
...earlyMenu,
|
|
2540
|
-
selectedIndex: Math.min(earlyMenu.candidates.length - 1, Number(value) - 1),
|
|
2541
|
-
});
|
|
2542
|
-
return;
|
|
2543
|
-
}
|
|
2544
|
-
if (key.return) {
|
|
2545
|
-
const candidate = earlyMenu.candidates[earlyMenu.selectedIndex];
|
|
2546
|
-
if (!candidate)
|
|
2547
|
-
return;
|
|
2548
|
-
const copying = clipboardWriter(candidate.text).then(() => {
|
|
2549
|
-
updateMenu(null);
|
|
2550
|
-
append({
|
|
2551
|
-
kind: 'local-result',
|
|
2552
|
-
text: `Copied ${candidate.label.toLowerCase()} to clipboard.`,
|
|
2553
|
-
});
|
|
2554
|
-
}, (error) => warn(error));
|
|
2555
|
-
onTurnChange?.(copying);
|
|
2556
|
-
void copying.finally(() => onTurnChange?.(null));
|
|
2557
|
-
}
|
|
2558
|
-
return;
|
|
2559
|
-
}
|
|
2560
3048
|
if (earlyMenu?.kind === 'agents') {
|
|
2561
3049
|
if (key.escape || value === '\u001B') {
|
|
2562
3050
|
updateMenu(null);
|
|
@@ -2682,11 +3170,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2682
3170
|
const rows = projectConfigRows(earlyMenu.snapshot, earlyMenu.query);
|
|
2683
3171
|
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
2684
3172
|
const tabs = [
|
|
2685
|
-
'settings',
|
|
2686
3173
|
'status',
|
|
2687
3174
|
'config',
|
|
2688
3175
|
'usage',
|
|
2689
|
-
'stats',
|
|
2690
3176
|
];
|
|
2691
3177
|
const index = tabs.indexOf(earlyMenu.tab);
|
|
2692
3178
|
const direction = key.leftArrow || (key.tab && key.shift) ? -1 : 1;
|
|
@@ -3433,19 +3919,6 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3433
3919
|
}
|
|
3434
3920
|
return;
|
|
3435
3921
|
}
|
|
3436
|
-
if (activeMenu.kind === 'status') {
|
|
3437
|
-
if (key.escape || value === '\u001B') {
|
|
3438
|
-
updateMenu(null);
|
|
3439
|
-
}
|
|
3440
|
-
else if (key.leftArrow || key.rightArrow || key.tab) {
|
|
3441
|
-
const direction = key.leftArrow || (key.tab && key.shift) ? -1 : 1;
|
|
3442
|
-
updateMenu({
|
|
3443
|
-
kind: 'status',
|
|
3444
|
-
tabIndex: Math.max(0, Math.min(4, activeMenu.tabIndex + direction)),
|
|
3445
|
-
});
|
|
3446
|
-
}
|
|
3447
|
-
return;
|
|
3448
|
-
}
|
|
3449
3922
|
if (activeMenu.kind === 'hooks') {
|
|
3450
3923
|
const event = activeMenu.configuration.events[activeMenu.eventIndex];
|
|
3451
3924
|
const matcher = event?.matchers[activeMenu.matcherIndex];
|
|
@@ -3648,6 +4121,10 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3648
4121
|
clearComposerInput();
|
|
3649
4122
|
updateMenu(null);
|
|
3650
4123
|
changeModel(model);
|
|
4124
|
+
void saveConfigSetting('model', model, configTarget).then(() => append({
|
|
4125
|
+
kind: 'local-result',
|
|
4126
|
+
text: `${model} set as default model for new sessions.`,
|
|
4127
|
+
}), (error) => warn(error));
|
|
3651
4128
|
}
|
|
3652
4129
|
}
|
|
3653
4130
|
else {
|
|
@@ -3674,6 +4151,38 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3674
4151
|
}
|
|
3675
4152
|
return;
|
|
3676
4153
|
}
|
|
4154
|
+
if (activeMenu.kind === 'copy') {
|
|
4155
|
+
if (key.escape || value === '\u001B') {
|
|
4156
|
+
updateMenu(null);
|
|
4157
|
+
}
|
|
4158
|
+
else if (key.upArrow || key.downArrow) {
|
|
4159
|
+
updateMenu({
|
|
4160
|
+
...activeMenu,
|
|
4161
|
+
selectedIndex: Math.max(0, Math.min(activeMenu.candidates.length - 1, activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
else if (/^[1-9]$/u.test(value)) {
|
|
4165
|
+
updateMenu({
|
|
4166
|
+
...activeMenu,
|
|
4167
|
+
selectedIndex: Math.min(activeMenu.candidates.length - 1, Number(value) - 1),
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
else if (key.return || value.toLowerCase() === 'w') {
|
|
4171
|
+
const candidate = activeMenu.candidates[activeMenu.selectedIndex];
|
|
4172
|
+
if (!candidate)
|
|
4173
|
+
return;
|
|
4174
|
+
updateMenu(null);
|
|
4175
|
+
if (value.toLowerCase() === 'w') {
|
|
4176
|
+
const writing = writeCopyCandidate(candidate).then((path) => append({ kind: 'local-result', text: `Written to ${path}` }), (error) => warn(error));
|
|
4177
|
+
onTurnChange?.(writing);
|
|
4178
|
+
void writing.finally(() => onTurnChange?.(null));
|
|
4179
|
+
}
|
|
4180
|
+
else {
|
|
4181
|
+
copyCandidate(candidate, candidate.kind === 'always');
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
3677
4186
|
if (activeMenu.kind === 'export') {
|
|
3678
4187
|
if (key.escape || value === '\u001B') {
|
|
3679
4188
|
updateMenu(null);
|
|
@@ -4040,19 +4549,29 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4040
4549
|
changeEffort(nextEffort);
|
|
4041
4550
|
return;
|
|
4042
4551
|
}
|
|
4043
|
-
if (!key.return
|
|
4552
|
+
if (!key.return)
|
|
4044
4553
|
return;
|
|
4045
4554
|
if (activeMenu.kind === 'model') {
|
|
4046
|
-
if (activeMenu.selectedIndex === 1) {
|
|
4047
|
-
updateMenu(null);
|
|
4048
|
-
changeModel(undefined);
|
|
4049
|
-
}
|
|
4050
|
-
else if (activeMenu.selectedIndex === 2) {
|
|
4555
|
+
if (activeMenu.selectedIndex === modelOptions.length - 1) {
|
|
4051
4556
|
clearComposerInput();
|
|
4052
4557
|
updateMenu({ kind: 'model-input' });
|
|
4558
|
+
return;
|
|
4053
4559
|
}
|
|
4054
|
-
|
|
4055
|
-
|
|
4560
|
+
const model = modelOptions[activeMenu.selectedIndex]?.model;
|
|
4561
|
+
updateMenu(null);
|
|
4562
|
+
if (activeMenu.selectedIndex === 0) {
|
|
4563
|
+
changeModel(undefined);
|
|
4564
|
+
void saveConfigSetting('model', 'default', configTarget).then(() => append({
|
|
4565
|
+
kind: 'local-result',
|
|
4566
|
+
text: 'Default model set for new sessions.',
|
|
4567
|
+
}), (error) => warn(error));
|
|
4568
|
+
}
|
|
4569
|
+
else if (model) {
|
|
4570
|
+
changeModel(model);
|
|
4571
|
+
void saveConfigSetting('model', model, configTarget).then(() => append({
|
|
4572
|
+
kind: 'local-result',
|
|
4573
|
+
text: `${model} set as default model for new sessions.`,
|
|
4574
|
+
}), (error) => warn(error));
|
|
4056
4575
|
}
|
|
4057
4576
|
}
|
|
4058
4577
|
else {
|
|
@@ -4149,11 +4668,14 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4149
4668
|
return;
|
|
4150
4669
|
}
|
|
4151
4670
|
const selectedCommand = matchingSlashCommands[selectedSlashCommandIndex];
|
|
4671
|
+
const commandNameQuery = inputRef.current.slice(1).toLocaleLowerCase();
|
|
4152
4672
|
const exactSelectedCommand = selectedCommand !== undefined &&
|
|
4153
|
-
|
|
4154
|
-
|
|
4673
|
+
commandNameQuery === selectedCommand.name.toLocaleLowerCase();
|
|
4674
|
+
const selectedCommandMatchesName = selectedCommand !== undefined &&
|
|
4675
|
+
selectedCommand.name.toLocaleLowerCase().startsWith(commandNameQuery);
|
|
4155
4676
|
if (selectedCommand &&
|
|
4156
|
-
(key.tab ||
|
|
4677
|
+
(key.tab ||
|
|
4678
|
+
(key.return && !exactSelectedCommand && selectedCommandMatchesName))) {
|
|
4157
4679
|
updateComposerInput(`/${selectedCommand.name} `);
|
|
4158
4680
|
setCommandPaletteOpen(false);
|
|
4159
4681
|
return;
|
|
@@ -4227,6 +4749,37 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4227
4749
|
: TUI_THEMES.indexOf(themeSettings.theme)),
|
|
4228
4750
|
});
|
|
4229
4751
|
}
|
|
4752
|
+
else if (prompt === '/vim') {
|
|
4753
|
+
const saving = (async () => {
|
|
4754
|
+
setBusy(true);
|
|
4755
|
+
try {
|
|
4756
|
+
const mode = runtimeSettingsRef.current.editor === 'vim' ? 'normal' : 'vim';
|
|
4757
|
+
const snapshot = await saveConfigSetting('editor', mode, configTarget);
|
|
4758
|
+
await reloadRuntimeSettings(snapshot);
|
|
4759
|
+
setVimInsertMode(true);
|
|
4760
|
+
append({
|
|
4761
|
+
kind: 'local-result',
|
|
4762
|
+
text: `Editor mode set to ${mode}. ${mode === 'vim'
|
|
4763
|
+
? 'Use Escape key to toggle between INSERT and NORMAL modes.'
|
|
4764
|
+
: 'Using standard (readline) keyboard bindings.'}`,
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
catch (error) {
|
|
4768
|
+
warn(error);
|
|
4769
|
+
}
|
|
4770
|
+
finally {
|
|
4771
|
+
setBusy(false);
|
|
4772
|
+
}
|
|
4773
|
+
})();
|
|
4774
|
+
onTurnChange?.(saving);
|
|
4775
|
+
void saving.finally(() => onTurnChange?.(null));
|
|
4776
|
+
}
|
|
4777
|
+
else if (prompt === '/output-style') {
|
|
4778
|
+
append({
|
|
4779
|
+
kind: 'local-result',
|
|
4780
|
+
text: '/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.',
|
|
4781
|
+
});
|
|
4782
|
+
}
|
|
4230
4783
|
else if (prompt === '/terminal-setup') {
|
|
4231
4784
|
const setup = (async () => {
|
|
4232
4785
|
setBusy(true);
|
|
@@ -4450,47 +5003,50 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4450
5003
|
}));
|
|
4451
5004
|
const measuredTokens = (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0);
|
|
4452
5005
|
const skillTokens = skills.reduce((total, skill) => total + skill.tokens, 0);
|
|
5006
|
+
const contextEntry = {
|
|
5007
|
+
kind: 'context',
|
|
5008
|
+
usedTokens: Math.max(measuredTokens, skillTokens),
|
|
5009
|
+
contextWindowTokens: runtimeDisplay.contextWindowTokens ?? 200_000,
|
|
5010
|
+
model: runtimeDisplay.model ?? 'provider default',
|
|
5011
|
+
skills,
|
|
5012
|
+
memoryFiles: [],
|
|
5013
|
+
};
|
|
4453
5014
|
setHistory((current) => [
|
|
4454
5015
|
...current,
|
|
4455
5016
|
{ kind: 'user', text: '/context' },
|
|
4456
|
-
|
|
4457
|
-
kind: 'context',
|
|
4458
|
-
usedTokens: Math.max(measuredTokens, skillTokens),
|
|
4459
|
-
contextWindowTokens: runtimeDisplay.contextWindowTokens ?? 200_000,
|
|
4460
|
-
skills,
|
|
4461
|
-
},
|
|
5017
|
+
contextEntry,
|
|
4462
5018
|
]);
|
|
4463
|
-
}
|
|
4464
|
-
else if (prompt === '/status') {
|
|
4465
|
-
updateMenu({ kind: 'status', tabIndex: 1 });
|
|
4466
|
-
}
|
|
4467
|
-
else if (prompt === '/config') {
|
|
4468
|
-
const initial = {
|
|
4469
|
-
kind: 'config',
|
|
4470
|
-
snapshot: { settings: {}, state: {} },
|
|
4471
|
-
tab: 'config',
|
|
4472
|
-
selectedIndex: 0,
|
|
4473
|
-
query: '',
|
|
4474
|
-
searchFocused: true,
|
|
4475
|
-
};
|
|
4476
|
-
updateMenu(initial);
|
|
4477
5019
|
const loading = (async () => {
|
|
4478
|
-
setBusy(true);
|
|
4479
5020
|
try {
|
|
4480
|
-
const
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
5021
|
+
const files = await memoryFilesLoader(keybindingsRoot, runtimeCwdRef.current);
|
|
5022
|
+
const memoryFiles = await Promise.all(files.entries
|
|
5023
|
+
.filter((entry) => entry.kind === 'file')
|
|
5024
|
+
.map(async (entry) => ({
|
|
5025
|
+
path: entry.displayPath,
|
|
5026
|
+
tokens: await estimateFileTokens(entry.path),
|
|
5027
|
+
})));
|
|
5028
|
+
setHistory((current) => {
|
|
5029
|
+
const next = [...current];
|
|
5030
|
+
const last = next.at(-1);
|
|
5031
|
+
if (last && last.kind === 'context') {
|
|
5032
|
+
next[next.length - 1] = { ...last, memoryFiles };
|
|
5033
|
+
}
|
|
5034
|
+
return next;
|
|
5035
|
+
});
|
|
4486
5036
|
}
|
|
4487
|
-
|
|
4488
|
-
|
|
5037
|
+
catch {
|
|
5038
|
+
// Leave the Memory files section empty if files cannot be read.
|
|
4489
5039
|
}
|
|
4490
5040
|
})();
|
|
4491
5041
|
onTurnChange?.(loading);
|
|
4492
5042
|
void loading.finally(() => onTurnChange?.(null));
|
|
4493
5043
|
}
|
|
5044
|
+
else if (prompt === '/status') {
|
|
5045
|
+
openSettings('status');
|
|
5046
|
+
}
|
|
5047
|
+
else if (prompt === '/config') {
|
|
5048
|
+
openSettings('config');
|
|
5049
|
+
}
|
|
4494
5050
|
else if (tuiCommand) {
|
|
4495
5051
|
const mode = tuiCommand[1];
|
|
4496
5052
|
if (!mode) {
|
|
@@ -4520,7 +5076,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4520
5076
|
}
|
|
4521
5077
|
}
|
|
4522
5078
|
else if (prompt === '/usage') {
|
|
4523
|
-
|
|
5079
|
+
openSettings('usage');
|
|
4524
5080
|
}
|
|
4525
5081
|
else if (prompt === '/update') {
|
|
4526
5082
|
append({
|
|
@@ -4600,11 +5156,26 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4600
5156
|
});
|
|
4601
5157
|
return (_jsx(TuiThemeProvider, { settings: themeSettings, children: _jsx(Box, { flexDirection: "column", children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && history.length === 0 && !sessionId ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }), externalEditorRequest !== null ||
|
|
4602
5158
|
keybindingsEditing ||
|
|
4603
|
-
memoryEditorRequest !== null ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (_jsxs(DialogFrame, { title: permission.
|
|
4604
|
-
|
|
4605
|
-
|
|
5159
|
+
memoryEditorRequest !== null ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (permission.kind === 'tool' && toolPermissionModel ? (_jsx(ToolPermissionDialog, { model: toolPermissionModel, selection: permissionSelection, feedbackMode: permissionFeedbackMode, feedback: input, ruleEditor: permissionRuleEditor, screenReader: axScreenReader })) : (_jsxs(DialogFrame, { title: `Retry interrupted ${permission.call.name}?`, screenReader: axScreenReader, children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { bold: true, children: describeTool(permission.call, sensitiveValues) }) }), _jsx(Text, { children: "Do you want to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 0, children: [selectionPrefix(permissionSelection === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 1, children: [selectionPrefix(permissionSelection === 1, axScreenReader), "2. No"] }), permissionFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
|
|
5160
|
+
(permissionSelection === 0
|
|
5161
|
+
? 'tell Praxis what to do next'
|
|
5162
|
+
: 'tell Praxis what to do differently')] })) : null, _jsx(Text, { dimColor: true, children: permissionFeedbackMode
|
|
5163
|
+
? 'Enter to submit · Tab to collapse · Esc to cancel'
|
|
5164
|
+
: 'Enter to confirm · Tab to add feedback · Esc to cancel' })] }))) : planApproval ? (_jsxs(DialogFrame, { title: "Ready to code?", screenReader: axScreenReader, children: [_jsx(Text, { children: "Here is Praxis's plan:" }), planApproval.request.plan ? (_jsx(Box, { borderStyle: axScreenReader ? undefined : 'classic', borderLeft: false, borderRight: false, paddingX: axScreenReader ? 0 : 1, marginY: 1, children: _jsx(Text, { children: planApproval.request.plan }) })) : null, _jsx(Text, { dimColor: true, children: "Praxis has written up a plan and is ready to execute. Would you like to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && planApprovalSelection === 0, children: [selectionPrefix(planApprovalSelection === 0, axScreenReader), "1.", ' ', runtimeSettings.useAutoModeDuringPlan
|
|
5165
|
+
? 'Yes, and use auto mode'
|
|
5166
|
+
: allowDangerouslySkipPermissions
|
|
5167
|
+
? 'Yes, and bypass permissions'
|
|
5168
|
+
: 'Yes, auto-accept edits'] }), _jsxs(Text, { inverse: !axScreenReader && planApprovalSelection === 1, children: [selectionPrefix(planApprovalSelection === 1, axScreenReader), "2. Yes, manually approve edits"] }), _jsxs(Text, { inverse: !axScreenReader && planApprovalSelection === 2, children: [selectionPrefix(planApprovalSelection === 2, axScreenReader), "3. No, keep planning"] }), planApprovalFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
|
|
5169
|
+
(planApprovalSelection === 2
|
|
5170
|
+
? 'Tell Praxis what to change'
|
|
5171
|
+
: 'Add feedback for implementation')] })) : null, _jsx(Text, { dimColor: true, children: planApproval.request.planPath }), _jsx(Text, { dimColor: true, children: planApprovalFeedbackMode
|
|
5172
|
+
? 'Enter to submit · Tab to collapse · Esc to cancel'
|
|
5173
|
+
: 'Enter to confirm · Tab to add feedback · Esc to cancel' })] })) : question ? (_jsxs(DialogFrame, { title: `${question.questions[question.index]?.header}: ${question.questions[question.index]?.question}`, screenReader: axScreenReader, children: [question.questions[question.index]?.options.map((option, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [index + 1, ". ", option.label, " \u2014 ", option.description] }), option.preview ? (_jsx(Text, { dimColor: true, children: option.preview })) : null] }, `${index}-${option.label}`))), _jsxs(Text, { children: [axScreenReader ? 'Current answer: ' : '› ', input || (axScreenReader ? '(empty)' : '')] }), _jsx(Text, { dimColor: true, children: question.questions[question.index]?.multiSelect
|
|
4606
5174
|
? 'Enter comma-separated option numbers or custom text · Esc cancels'
|
|
4607
|
-
: 'Enter one option number or custom text · Esc cancels' })] })) : elicitation ? (
|
|
5175
|
+
: 'Enter one option number or custom text · Esc cancels' })] })) : elicitation ? (elicitation.request.mode === 'url' ? (_jsx(McpElicitationUrl, { serverName: elicitation.request.serverName, message: elicitation.request.message, url: elicitation.request.url ?? '', waiting: elicitationUrlWaiting, actionLabel: elicitation.request.elicitationId
|
|
5176
|
+
? 'Skip confirmation'
|
|
5177
|
+
: 'Continue without waiting', selection: elicitationForm?.focusIndex ?? 0, screenReader: axScreenReader })) : (_jsx(McpElicitationForm, { serverName: elicitation.request.serverName, message: elicitation.request.message, state: elicitationForm ??
|
|
5178
|
+
createTuiElicitationForm(elicitation.request.requestedSchema), input: input, screenReader: axScreenReader }))) : menu?.kind === 'model-input' ? (_jsxs(DialogFrame, { title: "Enter model ID", screenReader: axScreenReader, children: [_jsx(Text, { dimColor: true, children: "Enter a model ID supported by the configured provider." }), _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: "Enter confirms \u00B7 Esc cancels" })] })) : menu?.kind === 'export-filename' ? (_jsxs(DialogFrame, { title: "Enter filename:", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["> ", input] }), _jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc to go back" })] })) : menu?.kind === 'compact-progress' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "\u273B Compacting conversation\u2026" }), _jsxs(Text, { children: ['▰'.repeat(Math.floor(compactProgress / 2)), '▱'.repeat(50 - Math.floor(compactProgress / 2)), ' ', compactProgress, "%"] })] })) : menu?.kind === 'btw' ? (_jsx(BtwPanel, { entries: btwHistory, selectedIndex: menu.selectedIndex, scrollOffset: menu.scrollOffset, copied: btwCopied, width: width, screenReader: axScreenReader })) : menu?.kind === 'rewind' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: " Rewind" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [' ', "Restore the code and/or conversation to the point before\u2026"] }), _jsx(Text, { children: " " }), (() => {
|
|
4608
5179
|
const window = rewindPointWindow(menu.points, menu.selectedIndex);
|
|
4609
5180
|
return (_jsxs(_Fragment, { children: [window.start > 0 ? (_jsxs(Text, { dimColor: true, children: [" \u2191 ", window.start, " more above"] })) : null, menu.points
|
|
4610
5181
|
.slice(window.start, window.end)
|
|
@@ -4641,27 +5212,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4641
5212
|
label: 'User settings',
|
|
4642
5213
|
description: 'Saved in at ~/.claude/settings.json',
|
|
4643
5214
|
},
|
|
4644
|
-
], selectedIndex: menu.selectedIndex, footer: "Enter to confirm \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === '
|
|
4645
|
-
label: candidate.label,
|
|
4646
|
-
description: candidate.description,
|
|
4647
|
-
})), selectedIndex: menu.selectedIndex, footer: "\u2191/\u2193 select \u00B7 Enter copies \u00B7 Esc cancels", width: width, screenReader: axScreenReader })) : menu.kind === 'agents' ? (_jsx(ListDashboard, { title: "Agents", rows: menu.agents.map((agent) => ({
|
|
5215
|
+
], selectedIndex: menu.selectedIndex, footer: "Enter to confirm \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === 'agents' ? (_jsx(ListDashboard, { title: "Agents", rows: menu.agents.map((agent) => ({
|
|
4648
5216
|
label: agent.name,
|
|
4649
5217
|
description: agent.description,
|
|
4650
|
-
})), emptyText: "No agents configured", selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'config' ? (_jsx(ConfigDashboard, { tab: menu.tab, snapshot: menu.snapshot, query: menu.query, selectedIndex: menu.selectedIndex, searchFocused: menu.searchFocused,
|
|
4651
|
-
{
|
|
4652
|
-
label: 'Provider',
|
|
4653
|
-
value: runtimeDisplay.model ?? 'default',
|
|
4654
|
-
},
|
|
4655
|
-
{
|
|
4656
|
-
label: 'Permission mode',
|
|
4657
|
-
value: runtimeDisplay.permissionMode ?? 'default',
|
|
4658
|
-
},
|
|
4659
|
-
], status: {
|
|
5218
|
+
})), emptyText: "No agents configured", selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'config' ? (_jsx(ConfigDashboard, { tab: menu.tab, snapshot: menu.snapshot, query: menu.query, selectedIndex: menu.selectedIndex, searchFocused: menu.searchFocused, status: {
|
|
4660
5219
|
version: runtimeDisplay.version,
|
|
5220
|
+
...(sessionName ? { sessionName } : {}),
|
|
4661
5221
|
sessionId: sessionId ?? 'new',
|
|
4662
5222
|
cwd: runtimeCwd,
|
|
5223
|
+
...(statusAuthSource
|
|
5224
|
+
? { authSource: statusAuthSource }
|
|
5225
|
+
: {}),
|
|
5226
|
+
...(statusBaseUrl ? { baseUrl: statusBaseUrl } : {}),
|
|
5227
|
+
...(statusProxy ? { proxy: statusProxy } : {}),
|
|
4663
5228
|
model: runtimeDisplay.model ?? 'default',
|
|
4664
|
-
settingSources:
|
|
5229
|
+
settingSources: statusSettingSources.split(', '),
|
|
4665
5230
|
}, usage: {
|
|
4666
5231
|
costUsd: costUsd ?? 0,
|
|
4667
5232
|
apiDurationMs: 0,
|
|
@@ -4669,7 +5234,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4669
5234
|
linesAdded: 0,
|
|
4670
5235
|
linesRemoved: 0,
|
|
4671
5236
|
usage: usage ?? { inputTokens: 0, outputTokens: 0 },
|
|
4672
|
-
},
|
|
5237
|
+
}, width: width, screenReader: axScreenReader })) : menu.kind === 'mcp' ? (_jsx(McpPanel, { model: menu.model, state: menu.state, width: width, screenReader: axScreenReader })) : menu.kind === 'tasks' ? (_jsx(TaskPanel, { tasks: menu.tasks, state: menu.state, width: width, screenReader: axScreenReader })) : menu.kind === 'memory' ? (_jsx(MemoryDashboard, { autoMemoryEnabled: menu.autoMemoryEnabled, entries: menu.entries, selectedIndex: menu.selectedIndex, openedIndex: menu.openedIndex, loading: menu.loading, width: width, screenReader: axScreenReader })) : menu.kind === 'hooks' ? (_jsx(HookDashboard, { configuration: menu.configuration, depth: menu.depth, eventIndex: menu.eventIndex, matcherIndex: menu.matcherIndex, hookIndex: menu.hookIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'list' ? (_jsx(ListDashboard, { title: menu.title, rows: menu.rows, emptyText: menu.emptyText, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'model' ? (_jsx(ModelMenu, { options: modelOptions, effort: runtimePreferences.effort, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'effort' ? (_jsx(SelectionMenu, { title: "Select effort", description: "Controls how much reasoning effort the provider should use.", options: EFFORT_OPTIONS.map((option) => ({
|
|
4673
5238
|
label: option,
|
|
4674
5239
|
description: option === 'low'
|
|
4675
5240
|
? 'Fastest and least deliberative.'
|
|
@@ -4686,7 +5251,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4686
5251
|
label: 'Save to file',
|
|
4687
5252
|
description: 'Save the conversation to a file in the current directory',
|
|
4688
5253
|
},
|
|
4689
|
-
], selectedIndex: menu.selectedIndex, footer: "Esc to cancel", width: width, screenReader: axScreenReader })) : null) : (_jsxs(_Fragment, { children: [commandPaletteVisible ? (_jsx(CommandPalette, { commands: matchingSlashCommands, selectedIndex: selectedSlashCommandIndex, width: width, screenReader: axScreenReader })) : null, filePickerVisible ? (_jsx(MentionPicker, { entries: matchingMentionEntries, selectedIndex: selectedFileIndex, width: width, screenReader: axScreenReader })) : null, exitConfirmation ? (_jsx(Text, { color: activePalette.warning, children: "Press Ctrl-C again to exit" })) : null, _jsx(Composer, { input: shellMode ? input.slice(1) : input, cursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor, shellMode: shellMode, busy: busy, clipboardBusy: clipboardBusy, status: status, display: runtimeDisplay, ...(usage === undefined ? {} : { usage }), ...(costUsd === undefined ? {} : { costUsd }), width: width, screenReader: axScreenReader, hasThinking: hasDetailedTranscript, thinkingExpanded: thinkingExpanded, reduceMotion: runtimeSettings.reduceMotion, progressBar: runtimeSettings.progressBar, ...(runtimeSettings.turnDuration
|
|
5254
|
+
], selectedIndex: menu.selectedIndex, footer: "Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === 'copy' ? (_jsx(SelectionMenu, { title: "Copy", description: "Select content to copy:", options: menu.candidates, selectedIndex: menu.selectedIndex, footer: "Enter to copy \u00B7 w to write to /tmp/claude \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : null) : (_jsxs(_Fragment, { children: [commandPaletteVisible ? (_jsx(CommandPalette, { commands: matchingSlashCommands, selectedIndex: selectedSlashCommandIndex, width: width, screenReader: axScreenReader })) : null, filePickerVisible ? (_jsx(MentionPicker, { entries: matchingMentionEntries, selectedIndex: selectedFileIndex, width: width, screenReader: axScreenReader })) : null, exitConfirmation ? (_jsx(Text, { color: activePalette.warning, children: "Press Ctrl-C again to exit" })) : null, _jsx(Composer, { input: shellMode ? input.slice(1) : input, cursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor, shellMode: shellMode, busy: busy, clipboardBusy: clipboardBusy, status: status, display: runtimeDisplay, ...(usage === undefined ? {} : { usage }), ...(costUsd === undefined ? {} : { costUsd }), width: width, screenReader: axScreenReader, hasThinking: hasDetailedTranscript, thinkingExpanded: thinkingExpanded, reduceMotion: runtimeSettings.reduceMotion, progressBar: runtimeSettings.progressBar, ...(runtimeSettings.turnDuration
|
|
4690
5255
|
? (() => {
|
|
4691
5256
|
const duration = formatTurnDuration(turnDuration);
|
|
4692
5257
|
return duration === undefined
|