praxis-agent 0.8.0 → 0.10.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 +3 -2
- package/dist/cli/interactive.js +243 -137
- package/dist/cli/tui/claude-style.d.ts +12 -14
- package/dist/cli/tui/claude-style.js +51 -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/runtime-interactions.d.ts +2 -0
- package/dist/cli/tui/runtime-interactions.js +44 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,11 +82,12 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
82
82
|
slash palette, tabbed help and shortcut surfaces, searchable resume picker,
|
|
83
83
|
restored active-branch conversation history, streaming and expandable
|
|
84
84
|
thinking, grouped multi-file reads, globally expandable tool results,
|
|
85
|
-
command-specific `/add-dir`, `/copy`, `/branch`, `/rename`, `/export`,
|
|
85
|
+
command-specific `/add-dir`, code-aware `/copy`, `/branch`, `/rename`, `/export`,
|
|
86
86
|
provider-free read-only shared `/hooks`, provider-backed `/compact`, native
|
|
87
87
|
`/rewind`, runtime `/cd`, transcript-free
|
|
88
88
|
`/btw` side questions with background-Agent handoff, interactive
|
|
89
|
-
`/background` terminal handoff, `/config
|
|
89
|
+
`/background` terminal handoff, unified `/status`/`/config`/`/usage` settings
|
|
90
|
+
tabs, `/mcp`, `/memory` shared
|
|
90
91
|
instruction and auto-memory access, and live extension-reload controls,
|
|
91
92
|
cursor/history composer, per-session model/effort/permission controls,
|
|
92
93
|
context/status/skill/task dashboards, prompt stash and continuation shortcuts,
|
package/dist/cli/interactive.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
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';
|
|
7
9
|
import { AgentRunCancelledError } from '../core/runtime.js';
|
|
8
10
|
import { claudePermissionActionKey, } from '../permissions/claude-permission-resolver.js';
|
|
9
11
|
import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
10
|
-
import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, PermissionDashboard, SelectionMenu, SessionPicker,
|
|
12
|
+
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
13
|
import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js';
|
|
12
14
|
import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
|
|
13
15
|
import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
|
|
@@ -29,7 +31,7 @@ import { setupTuiTerminal, terminalSetupTuiSlashCommand, } from './tui/terminal-
|
|
|
29
31
|
import { ConfigDashboard, projectConfigRows } from './tui/config-dashboard.js';
|
|
30
32
|
import { loadConfigSettings, saveConfigSetting, configSettingDefinition, resolveConfigSettingsLocation, } from './tui/config-settings.js';
|
|
31
33
|
import { projectRuntimeSettings, loadRuntimeSettings, } from './tui/runtime-settings.js';
|
|
32
|
-
import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap,
|
|
34
|
+
import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap, spinnerTip, shouldShowCopyPicker, } from './tui/runtime-interactions.js';
|
|
33
35
|
import { notifyTerminal, } from './tui/terminal-notifications.js';
|
|
34
36
|
import { McpPanel } from './tui/mcp-panel.js';
|
|
35
37
|
import { McpPanelController, mcpRuntimeFromSession, } from './tui/mcp-panel-controller.js';
|
|
@@ -38,6 +40,17 @@ import { TaskPanel, initialTuiTaskPanelState, projectTuiTasks, reconcileTuiTaskP
|
|
|
38
40
|
import { ClaudeMcpManagement } from '../mcp/claude-mcp-management.js';
|
|
39
41
|
const EMPTY_SLASH_COMMANDS = [];
|
|
40
42
|
const EMPTY_AGENTS = [];
|
|
43
|
+
const estimateFileTokens = async (path) => {
|
|
44
|
+
try {
|
|
45
|
+
const content = await readFile(path, 'utf8');
|
|
46
|
+
if (!content.trim())
|
|
47
|
+
return 0;
|
|
48
|
+
return Math.max(1, Math.round(content.length / 4));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
41
54
|
const EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
42
55
|
const PERMISSION_OPTIONS = [
|
|
43
56
|
{
|
|
@@ -271,6 +284,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
271
284
|
const [sessionId, setSessionId] = useState(resume?.sessionId ?? null);
|
|
272
285
|
const sessionIdRef = useRef(resume?.sessionId ?? null);
|
|
273
286
|
sessionIdRef.current = sessionId;
|
|
287
|
+
const [sessionName, setSessionName] = useState(null);
|
|
274
288
|
const [activeSessionSummary, setActiveSessionSummary] = useState(() => resume?.sessionId
|
|
275
289
|
? initialSessions.find((session) => session.sessionId === resume.sessionId)
|
|
276
290
|
: undefined);
|
|
@@ -422,6 +436,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
422
436
|
permissionMode: runtimePreferences.permissionMode,
|
|
423
437
|
...(contextWindowTokens === undefined ? {} : { contextWindowTokens }),
|
|
424
438
|
};
|
|
439
|
+
const statusAuthSource = process.env.PRAXIS_API_KEY
|
|
440
|
+
? 'PRAXIS_API_KEY'
|
|
441
|
+
: process.env.ANTHROPIC_API_KEY
|
|
442
|
+
? 'ANTHROPIC_API_KEY'
|
|
443
|
+
: undefined;
|
|
444
|
+
const statusBaseUrl = process.env.PRAXIS_BASE_URL;
|
|
445
|
+
const statusProxy = process.env.HTTPS_PROXY ??
|
|
446
|
+
process.env.https_proxy ??
|
|
447
|
+
process.env.ALL_PROXY ??
|
|
448
|
+
process.env.HTTP_PROXY;
|
|
449
|
+
const statusSettingSources = (() => {
|
|
450
|
+
const projectSettings = existsSync(join(runtimeCwd, '.claude', 'settings.json')) ||
|
|
451
|
+
existsSync(join(runtimeCwd, 'settings.json'));
|
|
452
|
+
return projectSettings ? 'User settings, Project settings' : 'User settings';
|
|
453
|
+
})();
|
|
425
454
|
const workspaceDirectories = [
|
|
426
455
|
runtimeCwd,
|
|
427
456
|
...runtimePreferences.additionalDirectories.filter((path) => path !== runtimeCwd),
|
|
@@ -438,23 +467,46 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
438
467
|
]
|
|
439
468
|
: []),
|
|
440
469
|
], [allowDangerouslySkipPermissions]);
|
|
441
|
-
const modelOptions = useMemo(() =>
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
: '
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
470
|
+
const modelOptions = useMemo(() => {
|
|
471
|
+
const current = runtimePreferences.model;
|
|
472
|
+
const options = [
|
|
473
|
+
{
|
|
474
|
+
label: 'Default (recommended)',
|
|
475
|
+
description: `Use the invocation default (currently ${runtimeDisplay.model ?? 'provider default'})`,
|
|
476
|
+
selected: current === undefined,
|
|
477
|
+
},
|
|
478
|
+
];
|
|
479
|
+
if ((process.env.PRAXIS_PROVIDER ?? 'openai') === 'anthropic') {
|
|
480
|
+
options.push({
|
|
481
|
+
label: 'Opus',
|
|
482
|
+
model: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? 'opus',
|
|
483
|
+
description: 'Most capable for complex work',
|
|
484
|
+
}, {
|
|
485
|
+
label: 'Sonnet',
|
|
486
|
+
model: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'sonnet',
|
|
487
|
+
description: 'Best for everyday tasks',
|
|
488
|
+
}, {
|
|
489
|
+
label: 'Haiku',
|
|
490
|
+
model: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? 'haiku',
|
|
491
|
+
description: 'Fastest for quick answers',
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
else if (current) {
|
|
495
|
+
options.push({
|
|
496
|
+
label: current,
|
|
497
|
+
model: current,
|
|
498
|
+
description: 'Current provider model',
|
|
499
|
+
selected: true,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
for (const option of options)
|
|
503
|
+
option.selected = option.model === current || (!option.model && !current);
|
|
504
|
+
options.push({
|
|
454
505
|
label: 'Enter a model ID…',
|
|
455
506
|
description: 'Use any model identifier supported by the configured provider.',
|
|
456
|
-
}
|
|
457
|
-
|
|
507
|
+
});
|
|
508
|
+
return options;
|
|
509
|
+
}, [runtimeDisplay.model, runtimePreferences.model]);
|
|
458
510
|
useEffect(() => {
|
|
459
511
|
setAvailableSlashCommands(slashCommands);
|
|
460
512
|
}, [slashCommands]);
|
|
@@ -1424,6 +1476,39 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1424
1476
|
onTurnChange?.(loading);
|
|
1425
1477
|
void loading.finally(() => onTurnChange?.(null));
|
|
1426
1478
|
};
|
|
1479
|
+
const writeCopyCandidate = async (candidate) => {
|
|
1480
|
+
const directory = join(tmpdir(), 'claude');
|
|
1481
|
+
await mkdir(directory, { recursive: true });
|
|
1482
|
+
const path = join(directory, candidate.filename);
|
|
1483
|
+
await writeFile(path, candidate.text, 'utf8');
|
|
1484
|
+
return path;
|
|
1485
|
+
};
|
|
1486
|
+
const copyCandidate = (candidate, savePreference = false) => {
|
|
1487
|
+
const copying = (async () => {
|
|
1488
|
+
try {
|
|
1489
|
+
if (savePreference) {
|
|
1490
|
+
const snapshot = await saveConfigSetting('copyFullResponse', true, configTarget);
|
|
1491
|
+
await reloadRuntimeSettings(snapshot);
|
|
1492
|
+
}
|
|
1493
|
+
await clipboardWriter(candidate.text);
|
|
1494
|
+
let result = `Copied to clipboard (${candidate.text.length} characters, ${candidate.text.split('\n').length} lines)`;
|
|
1495
|
+
try {
|
|
1496
|
+
result += `\nAlso written to ${await writeCopyCandidate(candidate)}`;
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
// Clipboard success is authoritative; the temp file is a fallback.
|
|
1500
|
+
}
|
|
1501
|
+
if (savePreference)
|
|
1502
|
+
result += '\nPreference saved. Use /config to change copyFullResponse';
|
|
1503
|
+
append({ kind: 'local-result', text: result });
|
|
1504
|
+
}
|
|
1505
|
+
catch (error) {
|
|
1506
|
+
warn(error);
|
|
1507
|
+
}
|
|
1508
|
+
})();
|
|
1509
|
+
onTurnChange?.(copying);
|
|
1510
|
+
void copying.finally(() => onTurnChange?.(null));
|
|
1511
|
+
};
|
|
1427
1512
|
const copyResponse = (position) => {
|
|
1428
1513
|
const response = [...history]
|
|
1429
1514
|
.reverse()
|
|
@@ -1435,21 +1520,48 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1435
1520
|
});
|
|
1436
1521
|
return;
|
|
1437
1522
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1523
|
+
const candidates = copyCandidates(response.text);
|
|
1524
|
+
const full = candidates[0];
|
|
1525
|
+
if (!full)
|
|
1526
|
+
return;
|
|
1527
|
+
if (runtimeSettingsRef.current.copyFullResponse ||
|
|
1528
|
+
!shouldShowCopyPicker(response.text)) {
|
|
1529
|
+
copyCandidate(full);
|
|
1445
1530
|
return;
|
|
1446
1531
|
}
|
|
1447
|
-
|
|
1448
|
-
kind: '
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1532
|
+
updateMenu({
|
|
1533
|
+
kind: 'copy',
|
|
1534
|
+
candidates,
|
|
1535
|
+
selectedIndex: 0,
|
|
1536
|
+
messageAge: position - 1,
|
|
1537
|
+
});
|
|
1538
|
+
};
|
|
1539
|
+
const openSettings = (tab) => {
|
|
1540
|
+
const initial = {
|
|
1541
|
+
kind: 'config',
|
|
1542
|
+
snapshot: { settings: {}, state: {} },
|
|
1543
|
+
tab,
|
|
1544
|
+
selectedIndex: 0,
|
|
1545
|
+
query: '',
|
|
1546
|
+
searchFocused: tab === 'config',
|
|
1547
|
+
};
|
|
1548
|
+
updateMenu(initial);
|
|
1549
|
+
const loading = (async () => {
|
|
1550
|
+
setBusy(true);
|
|
1551
|
+
try {
|
|
1552
|
+
const snapshot = await loadConfigSettings(configTarget);
|
|
1553
|
+
if (menuRef.current?.kind === 'config')
|
|
1554
|
+
updateMenu({ ...menuRef.current, snapshot });
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
warn(error);
|
|
1558
|
+
}
|
|
1559
|
+
finally {
|
|
1560
|
+
setBusy(false);
|
|
1561
|
+
}
|
|
1562
|
+
})();
|
|
1563
|
+
onTurnChange?.(loading);
|
|
1564
|
+
void loading.finally(() => onTurnChange?.(null));
|
|
1453
1565
|
};
|
|
1454
1566
|
const exportConversation = (method) => {
|
|
1455
1567
|
if (method === 'file') {
|
|
@@ -1808,6 +1920,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
1808
1920
|
if (!name)
|
|
1809
1921
|
throw new Error('Could not generate a session name');
|
|
1810
1922
|
await commands.rename(sessionId, name);
|
|
1923
|
+
setSessionName(name);
|
|
1811
1924
|
append({ kind: 'local-result', text: `Session renamed to: ${name}` });
|
|
1812
1925
|
}
|
|
1813
1926
|
catch (error) {
|
|
@@ -2522,41 +2635,6 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2522
2635
|
return;
|
|
2523
2636
|
}
|
|
2524
2637
|
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
2638
|
if (earlyMenu?.kind === 'agents') {
|
|
2561
2639
|
if (key.escape || value === '\u001B') {
|
|
2562
2640
|
updateMenu(null);
|
|
@@ -2682,11 +2760,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
2682
2760
|
const rows = projectConfigRows(earlyMenu.snapshot, earlyMenu.query);
|
|
2683
2761
|
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
2684
2762
|
const tabs = [
|
|
2685
|
-
'settings',
|
|
2686
2763
|
'status',
|
|
2687
2764
|
'config',
|
|
2688
2765
|
'usage',
|
|
2689
|
-
'stats',
|
|
2690
2766
|
];
|
|
2691
2767
|
const index = tabs.indexOf(earlyMenu.tab);
|
|
2692
2768
|
const direction = key.leftArrow || (key.tab && key.shift) ? -1 : 1;
|
|
@@ -3433,19 +3509,6 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3433
3509
|
}
|
|
3434
3510
|
return;
|
|
3435
3511
|
}
|
|
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
3512
|
if (activeMenu.kind === 'hooks') {
|
|
3450
3513
|
const event = activeMenu.configuration.events[activeMenu.eventIndex];
|
|
3451
3514
|
const matcher = event?.matchers[activeMenu.matcherIndex];
|
|
@@ -3648,6 +3711,10 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3648
3711
|
clearComposerInput();
|
|
3649
3712
|
updateMenu(null);
|
|
3650
3713
|
changeModel(model);
|
|
3714
|
+
void saveConfigSetting('model', model, configTarget).then(() => append({
|
|
3715
|
+
kind: 'local-result',
|
|
3716
|
+
text: `${model} set as default model for new sessions.`,
|
|
3717
|
+
}), (error) => warn(error));
|
|
3651
3718
|
}
|
|
3652
3719
|
}
|
|
3653
3720
|
else {
|
|
@@ -3674,6 +3741,38 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3674
3741
|
}
|
|
3675
3742
|
return;
|
|
3676
3743
|
}
|
|
3744
|
+
if (activeMenu.kind === 'copy') {
|
|
3745
|
+
if (key.escape || value === '\u001B') {
|
|
3746
|
+
updateMenu(null);
|
|
3747
|
+
}
|
|
3748
|
+
else if (key.upArrow || key.downArrow) {
|
|
3749
|
+
updateMenu({
|
|
3750
|
+
...activeMenu,
|
|
3751
|
+
selectedIndex: Math.max(0, Math.min(activeMenu.candidates.length - 1, activeMenu.selectedIndex + (key.upArrow ? -1 : 1))),
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
else if (/^[1-9]$/u.test(value)) {
|
|
3755
|
+
updateMenu({
|
|
3756
|
+
...activeMenu,
|
|
3757
|
+
selectedIndex: Math.min(activeMenu.candidates.length - 1, Number(value) - 1),
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
else if (key.return || value.toLowerCase() === 'w') {
|
|
3761
|
+
const candidate = activeMenu.candidates[activeMenu.selectedIndex];
|
|
3762
|
+
if (!candidate)
|
|
3763
|
+
return;
|
|
3764
|
+
updateMenu(null);
|
|
3765
|
+
if (value.toLowerCase() === 'w') {
|
|
3766
|
+
const writing = writeCopyCandidate(candidate).then((path) => append({ kind: 'local-result', text: `Written to ${path}` }), (error) => warn(error));
|
|
3767
|
+
onTurnChange?.(writing);
|
|
3768
|
+
void writing.finally(() => onTurnChange?.(null));
|
|
3769
|
+
}
|
|
3770
|
+
else {
|
|
3771
|
+
copyCandidate(candidate, candidate.kind === 'always');
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3677
3776
|
if (activeMenu.kind === 'export') {
|
|
3678
3777
|
if (key.escape || value === '\u001B') {
|
|
3679
3778
|
updateMenu(null);
|
|
@@ -4040,19 +4139,29 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4040
4139
|
changeEffort(nextEffort);
|
|
4041
4140
|
return;
|
|
4042
4141
|
}
|
|
4043
|
-
if (!key.return
|
|
4142
|
+
if (!key.return)
|
|
4044
4143
|
return;
|
|
4045
4144
|
if (activeMenu.kind === 'model') {
|
|
4046
|
-
if (activeMenu.selectedIndex === 1) {
|
|
4047
|
-
updateMenu(null);
|
|
4048
|
-
changeModel(undefined);
|
|
4049
|
-
}
|
|
4050
|
-
else if (activeMenu.selectedIndex === 2) {
|
|
4145
|
+
if (activeMenu.selectedIndex === modelOptions.length - 1) {
|
|
4051
4146
|
clearComposerInput();
|
|
4052
4147
|
updateMenu({ kind: 'model-input' });
|
|
4148
|
+
return;
|
|
4053
4149
|
}
|
|
4054
|
-
|
|
4055
|
-
|
|
4150
|
+
const model = modelOptions[activeMenu.selectedIndex]?.model;
|
|
4151
|
+
updateMenu(null);
|
|
4152
|
+
if (activeMenu.selectedIndex === 0) {
|
|
4153
|
+
changeModel(undefined);
|
|
4154
|
+
void saveConfigSetting('model', 'default', configTarget).then(() => append({
|
|
4155
|
+
kind: 'local-result',
|
|
4156
|
+
text: 'Default model set for new sessions.',
|
|
4157
|
+
}), (error) => warn(error));
|
|
4158
|
+
}
|
|
4159
|
+
else if (model) {
|
|
4160
|
+
changeModel(model);
|
|
4161
|
+
void saveConfigSetting('model', model, configTarget).then(() => append({
|
|
4162
|
+
kind: 'local-result',
|
|
4163
|
+
text: `${model} set as default model for new sessions.`,
|
|
4164
|
+
}), (error) => warn(error));
|
|
4056
4165
|
}
|
|
4057
4166
|
}
|
|
4058
4167
|
else {
|
|
@@ -4450,47 +4559,50 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4450
4559
|
}));
|
|
4451
4560
|
const measuredTokens = (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0);
|
|
4452
4561
|
const skillTokens = skills.reduce((total, skill) => total + skill.tokens, 0);
|
|
4562
|
+
const contextEntry = {
|
|
4563
|
+
kind: 'context',
|
|
4564
|
+
usedTokens: Math.max(measuredTokens, skillTokens),
|
|
4565
|
+
contextWindowTokens: runtimeDisplay.contextWindowTokens ?? 200_000,
|
|
4566
|
+
model: runtimeDisplay.model ?? 'provider default',
|
|
4567
|
+
skills,
|
|
4568
|
+
memoryFiles: [],
|
|
4569
|
+
};
|
|
4453
4570
|
setHistory((current) => [
|
|
4454
4571
|
...current,
|
|
4455
4572
|
{ kind: 'user', text: '/context' },
|
|
4456
|
-
|
|
4457
|
-
kind: 'context',
|
|
4458
|
-
usedTokens: Math.max(measuredTokens, skillTokens),
|
|
4459
|
-
contextWindowTokens: runtimeDisplay.contextWindowTokens ?? 200_000,
|
|
4460
|
-
skills,
|
|
4461
|
-
},
|
|
4573
|
+
contextEntry,
|
|
4462
4574
|
]);
|
|
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
4575
|
const loading = (async () => {
|
|
4478
|
-
setBusy(true);
|
|
4479
4576
|
try {
|
|
4480
|
-
const
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4577
|
+
const files = await memoryFilesLoader(keybindingsRoot, runtimeCwdRef.current);
|
|
4578
|
+
const memoryFiles = await Promise.all(files.entries
|
|
4579
|
+
.filter((entry) => entry.kind === 'file')
|
|
4580
|
+
.map(async (entry) => ({
|
|
4581
|
+
path: entry.displayPath,
|
|
4582
|
+
tokens: await estimateFileTokens(entry.path),
|
|
4583
|
+
})));
|
|
4584
|
+
setHistory((current) => {
|
|
4585
|
+
const next = [...current];
|
|
4586
|
+
const last = next.at(-1);
|
|
4587
|
+
if (last && last.kind === 'context') {
|
|
4588
|
+
next[next.length - 1] = { ...last, memoryFiles };
|
|
4589
|
+
}
|
|
4590
|
+
return next;
|
|
4591
|
+
});
|
|
4486
4592
|
}
|
|
4487
|
-
|
|
4488
|
-
|
|
4593
|
+
catch {
|
|
4594
|
+
// Leave the Memory files section empty if files cannot be read.
|
|
4489
4595
|
}
|
|
4490
4596
|
})();
|
|
4491
4597
|
onTurnChange?.(loading);
|
|
4492
4598
|
void loading.finally(() => onTurnChange?.(null));
|
|
4493
4599
|
}
|
|
4600
|
+
else if (prompt === '/status') {
|
|
4601
|
+
openSettings('status');
|
|
4602
|
+
}
|
|
4603
|
+
else if (prompt === '/config') {
|
|
4604
|
+
openSettings('config');
|
|
4605
|
+
}
|
|
4494
4606
|
else if (tuiCommand) {
|
|
4495
4607
|
const mode = tuiCommand[1];
|
|
4496
4608
|
if (!mode) {
|
|
@@ -4520,7 +4632,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4520
4632
|
}
|
|
4521
4633
|
}
|
|
4522
4634
|
else if (prompt === '/usage') {
|
|
4523
|
-
|
|
4635
|
+
openSettings('usage');
|
|
4524
4636
|
}
|
|
4525
4637
|
else if (prompt === '/update') {
|
|
4526
4638
|
append({
|
|
@@ -4641,27 +4753,21 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4641
4753
|
label: 'User settings',
|
|
4642
4754
|
description: 'Saved in at ~/.claude/settings.json',
|
|
4643
4755
|
},
|
|
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) => ({
|
|
4756
|
+
], 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
4757
|
label: agent.name,
|
|
4649
4758
|
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: {
|
|
4759
|
+
})), 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
4760
|
version: runtimeDisplay.version,
|
|
4761
|
+
...(sessionName ? { sessionName } : {}),
|
|
4661
4762
|
sessionId: sessionId ?? 'new',
|
|
4662
4763
|
cwd: runtimeCwd,
|
|
4764
|
+
...(statusAuthSource
|
|
4765
|
+
? { authSource: statusAuthSource }
|
|
4766
|
+
: {}),
|
|
4767
|
+
...(statusBaseUrl ? { baseUrl: statusBaseUrl } : {}),
|
|
4768
|
+
...(statusProxy ? { proxy: statusProxy } : {}),
|
|
4663
4769
|
model: runtimeDisplay.model ?? 'default',
|
|
4664
|
-
settingSources:
|
|
4770
|
+
settingSources: statusSettingSources.split(', '),
|
|
4665
4771
|
}, usage: {
|
|
4666
4772
|
costUsd: costUsd ?? 0,
|
|
4667
4773
|
apiDurationMs: 0,
|
|
@@ -4669,7 +4775,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4669
4775
|
linesAdded: 0,
|
|
4670
4776
|
linesRemoved: 0,
|
|
4671
4777
|
usage: usage ?? { inputTokens: 0, outputTokens: 0 },
|
|
4672
|
-
},
|
|
4778
|
+
}, 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
4779
|
label: option,
|
|
4674
4780
|
description: option === 'low'
|
|
4675
4781
|
? 'Fastest and least deliberative.'
|
|
@@ -4686,7 +4792,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4686
4792
|
label: 'Save to file',
|
|
4687
4793
|
description: 'Save the conversation to a file in the current directory',
|
|
4688
4794
|
},
|
|
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
|
|
4795
|
+
], 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
4796
|
? (() => {
|
|
4691
4797
|
const duration = formatTurnDuration(turnDuration);
|
|
4692
4798
|
return duration === undefined
|
|
@@ -32,10 +32,15 @@ export type TranscriptItem = {
|
|
|
32
32
|
kind: 'context';
|
|
33
33
|
usedTokens: number;
|
|
34
34
|
contextWindowTokens: number;
|
|
35
|
+
model?: string;
|
|
35
36
|
skills: readonly {
|
|
36
37
|
name: string;
|
|
37
38
|
tokens: number;
|
|
38
39
|
}[];
|
|
40
|
+
memoryFiles: readonly {
|
|
41
|
+
path: string;
|
|
42
|
+
tokens: number;
|
|
43
|
+
}[];
|
|
39
44
|
} | {
|
|
40
45
|
kind: 'tool';
|
|
41
46
|
call: ModelToolCall;
|
|
@@ -124,20 +129,6 @@ export declare function CustomThemeEditor({ theme, token, value, tokens, selecte
|
|
|
124
129
|
width: number;
|
|
125
130
|
screenReader: boolean;
|
|
126
131
|
}): import("react").JSX.Element;
|
|
127
|
-
export declare function StatusDashboard({ tabIndex, version, sessionId, display, usage, costUsd, turnCount, toolCount, commandCount, detailedTranscript, width, screenReader, }: {
|
|
128
|
-
tabIndex: number;
|
|
129
|
-
version: string;
|
|
130
|
-
sessionId: string | null;
|
|
131
|
-
display: TuiDisplayMetadata;
|
|
132
|
-
usage?: ModelUsage;
|
|
133
|
-
costUsd?: number;
|
|
134
|
-
turnCount: number;
|
|
135
|
-
toolCount: number;
|
|
136
|
-
commandCount: number;
|
|
137
|
-
detailedTranscript: boolean;
|
|
138
|
-
width: number;
|
|
139
|
-
screenReader: boolean;
|
|
140
|
-
}): import("react").JSX.Element;
|
|
141
132
|
export declare function ListDashboard({ title, rows, emptyText, selectedIndex, width, screenReader, }: {
|
|
142
133
|
title: string;
|
|
143
134
|
rows: readonly {
|
|
@@ -229,6 +220,13 @@ export declare function SelectionMenu({ title, description, options, selectedInd
|
|
|
229
220
|
width: number;
|
|
230
221
|
screenReader: boolean;
|
|
231
222
|
}): import("react").JSX.Element;
|
|
223
|
+
export declare function ModelMenu({ options, effort, selectedIndex, width, screenReader, }: {
|
|
224
|
+
options: readonly TuiSelectionOption[];
|
|
225
|
+
effort: string;
|
|
226
|
+
selectedIndex: number;
|
|
227
|
+
width: number;
|
|
228
|
+
screenReader: boolean;
|
|
229
|
+
}): import("react").JSX.Element;
|
|
232
230
|
export declare function ExternalEditorWait({ screenReader, }: {
|
|
233
231
|
screenReader: boolean;
|
|
234
232
|
}): import("react").JSX.Element;
|
|
@@ -46,14 +46,20 @@ function permissionLabel(mode) {
|
|
|
46
46
|
}
|
|
47
47
|
function selectionPrefix(selected, screenReader) {
|
|
48
48
|
if (selected)
|
|
49
|
-
return screenReader ? 'Selected: ' : '❯ ';
|
|
50
|
-
return screenReader ? '' : '
|
|
49
|
+
return screenReader ? 'Selected: ' : ' ❯ ';
|
|
50
|
+
return screenReader ? '' : ' ';
|
|
51
51
|
}
|
|
52
52
|
export function WelcomePanel({ display, width, }) {
|
|
53
53
|
const palette = useTuiPalette();
|
|
54
54
|
const panelWidth = Math.min(100, Math.max(32, width));
|
|
55
55
|
const wide = panelWidth >= 68;
|
|
56
|
-
|
|
56
|
+
const brand = 'Praxis';
|
|
57
|
+
// Claude 2.1.208 embeds the title in the top border row:
|
|
58
|
+
// ╭───Claude Code v2.1.208 ───...───╮
|
|
59
|
+
// Title block = "╭" + "───" + "Praxis Code vX.Y.Z" + " " = 4 + title.length + 1 chars.
|
|
60
|
+
const title = `${brand} Code v${display.version}`;
|
|
61
|
+
const fill = Math.max(1, panelWidth - title.length - 6);
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", width: panelWidth, children: [_jsxs(Text, { color: palette.muted, children: ['╭───', _jsx(Text, { color: palette.brand, bold: true, children: brand }), ` Code v${display.version} `, _jsx(Text, { dimColor: true, children: '─'.repeat(fill) }), '╮'] }), _jsx(Box, { borderStyle: "round", borderColor: palette.muted, borderTop: false, flexDirection: "column", width: panelWidth, paddingX: 1, children: _jsxs(Box, { flexDirection: wide ? 'row' : 'column', children: [_jsxs(Box, { alignItems: wide ? 'center' : undefined, flexDirection: "column", width: wide ? '50%' : '100%', children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Welcome back!" }), _jsx(Text, { children: " " }), _jsx(Text, { color: palette.brand, bold: true, children: "\u2590\u259B\u2588\u2588\u2588\u259C\u258C" }), _jsx(Text, { color: palette.brand, children: "\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598" }), _jsx(Text, { color: palette.brand, children: " \u2598\u2598 \u259D\u259D" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [display.model ?? 'provider default', display.effort ? (_jsxs(Text, { dimColor: true, children: [" \u00B7 ", display.effort, " effort"] })) : null] }), _jsx(Text, { dimColor: true, children: compactPath(display.cwd) })] }), _jsxs(Box, { flexDirection: "column", width: wide ? '50%' : '100%', marginTop: wide ? 0 : 1, children: [_jsx(Text, { bold: true, children: "Tips for getting started" }), _jsx(Text, { children: "Run /init to create a CLAUDE.md file with instructions for Claude" }), _jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Text, { bold: true, children: "What's new" }), _jsx(Text, { children: 'Subagent forking is now on by default: a `subagent_type: "fork"` subagent inherits the full conversation and prompt cache, and non-teammate agent spawns in interactive sessions now run in the background by default' }), _jsx(Text, { children: 'Type `@` in the prompt to mention another Claude session by name; Claude then uses `SendMessage` to reach that session directly' }), _jsx(Text, { children: '`SendMessage` now delivers to a bare name that exactly matches one live session, instead of asking to confirm with a ref first' }), _jsx(Text, { dimColor: true, children: "/release-notes for more" })] })] }) })] }));
|
|
57
63
|
}
|
|
58
64
|
function InlineText({ text }) {
|
|
59
65
|
const palette = useTuiPalette();
|
|
@@ -177,19 +183,27 @@ function ThinkingBlock({ text, active, expanded, screenReader, }) {
|
|
|
177
183
|
const showFull = screenReader || active || expanded;
|
|
178
184
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { ...(!screenReader ? { color: palette.accent } : {}), dimColor: !screenReader, italic: !screenReader, children: [screenReader ? 'Thinking:' : '✻ ', active ? 'Thinking…' : 'Thought for a moment', !showFull && summary ? ` · ${summary.slice(0, 160)}` : ''] }), showFull && text ? (_jsx(Box, { marginLeft: screenReader ? 0 : 2, flexDirection: "column", children: _jsx(MarkdownText, { text: text }) })) : !screenReader && summary.length > 160 ? (_jsx(Text, { dimColor: true, children: " ctrl+o to expand thinking" })) : null] }));
|
|
179
185
|
}
|
|
180
|
-
function
|
|
186
|
+
function compactTokens(tokens) {
|
|
187
|
+
if (tokens >= 1_000_000)
|
|
188
|
+
return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/u, '')}m`;
|
|
189
|
+
if (tokens >= 1_000)
|
|
190
|
+
return `${(tokens / 1_000).toFixed(1).replace(/\.0$/u, '')}k`;
|
|
191
|
+
return String(tokens);
|
|
192
|
+
}
|
|
193
|
+
function percent(tokens, total) {
|
|
194
|
+
return `${Math.round((tokens / Math.max(1, total)) * 100 * 10) / 10}%`;
|
|
195
|
+
}
|
|
196
|
+
function ContextUsageBlock({ usedTokens, contextWindowTokens, model, skills, memoryFiles, screenReader, }) {
|
|
181
197
|
const totalTokens = Math.max(1, contextWindowTokens);
|
|
182
198
|
const compactBuffer = Math.round(totalTokens * 0.165);
|
|
183
199
|
const usable = Math.max(1, totalTokens - compactBuffer);
|
|
184
200
|
if (screenReader) {
|
|
185
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Context Usage" }), _jsxs(Text, { children: [usedTokens.toLocaleString(), "
|
|
201
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Context Usage" }), _jsxs(Text, { children: [model ?? 'provider default', " \u00B7 ", usedTokens.toLocaleString(), "/", totalTokens.toLocaleString(), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsxs(Text, { children: ["Autocompact buffer: ", compactBuffer.toLocaleString(), " tokens"] }), _jsxs(Text, { children: ["Skills: ", skills.map(({ name }) => name).join(', ') || 'none'] })] }));
|
|
186
202
|
}
|
|
187
|
-
const usedCells = Math.min(
|
|
188
|
-
const bufferCells = Math.min(
|
|
189
|
-
const cells = Array.from({ length:
|
|
190
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { bold: true, children: "Context Usage" }), _jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", marginRight: 2, children: Array.from({ length:
|
|
191
|
-
.reduce((total, skill) => total + skill.tokens, 0)
|
|
192
|
-
.toLocaleString(), ' ', "tokens"] }), _jsxs(Text, { children: ["\u26F6 Free space: ", Math.max(0, usable - usedTokens).toLocaleString()] }), _jsxs(Text, { children: ["\u26DD Autocompact buffer: ", compactBuffer.toLocaleString(), " tokens"] })] })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Auto-compact window: ", totalTokens.toLocaleString(), " tokens"] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Skills \u00B7 /skills" }), skills.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No skills loaded" })) : (skills.map((skill, index) => (_jsxs(Text, { dimColor: true, children: [index === skills.length - 1 ? '└' : '├', " ", skill.name, ": ~", skill.tokens, " tokens"] }, skill.name))))] }));
|
|
203
|
+
const usedCells = Math.min(25, Math.round((usedTokens / totalTokens) * 25));
|
|
204
|
+
const bufferCells = Math.min(25 - usedCells, Math.round((compactBuffer / totalTokens) * 25));
|
|
205
|
+
const cells = Array.from({ length: 25 }, (_, index) => index < usedCells ? '⛁' : index >= 25 - bufferCells ? '⛝' : '⛶');
|
|
206
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { bold: true, children: "Context Usage" }), _jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", marginRight: 2, children: Array.from({ length: 5 }, (_, row) => (_jsx(Text, { children: cells.slice(row * 5, row * 5 + 5).join(' ') }, row))) }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [model ?? 'provider default', " \u00B7 ", compactTokens(usedTokens), "/", compactTokens(totalTokens), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, italic: true, children: "Estimated usage by category" }), _jsxs(Text, { children: ["\u26C1 Messages and other context: ", compactTokens(usedTokens), " tokens (", percent(usedTokens, totalTokens), ")"] }), _jsxs(Text, { children: ["\u26F6 Free space: ", compactTokens(Math.max(0, usable - usedTokens)), " (", percent(Math.max(0, usable - usedTokens), totalTokens), ")"] }), _jsxs(Text, { children: ["\u26DD Autocompact buffer: ", compactTokens(compactBuffer), " tokens (", percent(compactBuffer, totalTokens), ")"] })] })] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Memory files \u00B7 /memory" }), memoryFiles.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No memory files" })) : (memoryFiles.map((file, index) => (_jsxs(Text, { dimColor: true, children: [index === memoryFiles.length - 1 ? '└' : '├', " ", file.path, ":", ' ', file.tokens, " tokens"] }, file.path)))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Skills \u00B7 /skills" }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Loaded" }), skills.length === 0 ? (_jsx(Text, { dimColor: true, children: "\u2514 No skills loaded" })) : (skills.map((skill, index) => (_jsxs(Text, { dimColor: true, children: [index === skills.length - 1 ? '└' : '├', " ", skill.name, ": ~", skill.tokens, " tokens"] }, skill.name))))] }));
|
|
193
207
|
}
|
|
194
208
|
export function MarkdownText({ text }) {
|
|
195
209
|
const lines = text.split('\n');
|
|
@@ -397,7 +411,7 @@ export function ThemePicker({ currentTheme, selectedIndex, syntaxHighlightingDis
|
|
|
397
411
|
? {}
|
|
398
412
|
: { backgroundColor: syntax.addedBackground }), children: [' console.', _jsx(Text, { ...syntaxColor(syntax.identifier), children: "log" }), '(', _jsx(Text, { ...syntaxColor(syntax.string), children: "\"Hello, " }), _jsx(Text, { ...(syntaxHighlightingDisabled || !syntax.addedHighlight
|
|
399
413
|
? {}
|
|
400
|
-
: { backgroundColor: syntax.addedHighlight }), children: "
|
|
414
|
+
: { backgroundColor: syntax.addedHighlight }), children: "Claude" }), _jsx(Text, { ...syntaxColor(syntax.string), children: "!\"" }), '); '] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: " 3 " }), _jsx(Text, { ...syntaxColor(syntax.text), children: '}' })] }), _jsxs(Text, { dimColor: true, children: [' ', '╌'.repeat(Math.max(1, Math.min(96, width - 3)))] })] })) : null, _jsxs(Text, { dimColor: true, children: [' ', selected?.theme === '__new__'
|
|
401
415
|
? 'Enter to create a custom theme'
|
|
402
416
|
: syntaxHighlightingDisabled
|
|
403
417
|
? 'Syntax highlighting disabled (ctrl+t to enable)'
|
|
@@ -408,55 +422,6 @@ export function CustomThemeEditor({ theme, token, value, tokens = [], selectedIn
|
|
|
408
422
|
.slice(selectedIndex, selectedIndex + 8)
|
|
409
423
|
.map((entry, index) => (_jsxs(Text, { inverse: index === 0, children: [index === 0 ? '❯ ' : ' ', "\u2588\u2588 ", entry, theme.overrides[entry] === undefined ? '' : ' (custom)'] }, entry))), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to nav \u00B7 Enter to edit \u00B7 Tab to reset \u00B7 Esc to done" })] })), screenReader ? _jsx(Text, { children: "Editing custom theme token" }) : null] }));
|
|
410
424
|
}
|
|
411
|
-
export function StatusDashboard({ tabIndex, version, sessionId, display, usage, costUsd, turnCount, toolCount, commandCount, detailedTranscript, width, screenReader, }) {
|
|
412
|
-
const tabs = ['Settings', 'Status', 'Config', 'Usage', 'Stats'];
|
|
413
|
-
const rows = tabIndex === 0
|
|
414
|
-
? [
|
|
415
|
-
['Thinking mode', 'provider controlled'],
|
|
416
|
-
['Verbose output', detailedTranscript ? 'true' : 'false'],
|
|
417
|
-
['Default permission mode', permissionLabel(display.permissionMode)],
|
|
418
|
-
['Context compaction', 'automatic'],
|
|
419
|
-
['Shared Claude data', 'enabled'],
|
|
420
|
-
]
|
|
421
|
-
: tabIndex === 1
|
|
422
|
-
? [
|
|
423
|
-
['Version', version],
|
|
424
|
-
['Session ID', sessionId ?? 'new session'],
|
|
425
|
-
['cwd', display.cwd],
|
|
426
|
-
['Model', display.model ?? 'provider default'],
|
|
427
|
-
['Permission mode', permissionLabel(display.permissionMode)],
|
|
428
|
-
]
|
|
429
|
-
: tabIndex === 2
|
|
430
|
-
? [
|
|
431
|
-
['Model', display.model ?? 'provider default'],
|
|
432
|
-
['Effort', display.effort ?? 'high'],
|
|
433
|
-
[
|
|
434
|
-
'Context window',
|
|
435
|
-
String(display.contextWindowTokens ?? 'provider default'),
|
|
436
|
-
],
|
|
437
|
-
['Available commands', String(commandCount)],
|
|
438
|
-
]
|
|
439
|
-
: tabIndex === 3
|
|
440
|
-
? [
|
|
441
|
-
['Input tokens', String(usage?.inputTokens ?? 0)],
|
|
442
|
-
['Output tokens', String(usage?.outputTokens ?? 0)],
|
|
443
|
-
[
|
|
444
|
-
'Session cost',
|
|
445
|
-
costUsd === undefined
|
|
446
|
-
? 'unavailable'
|
|
447
|
-
: `$${costUsd.toFixed(4)}`,
|
|
448
|
-
],
|
|
449
|
-
]
|
|
450
|
-
: [
|
|
451
|
-
['Turns', String(turnCount)],
|
|
452
|
-
['Tool calls', String(toolCount)],
|
|
453
|
-
[
|
|
454
|
-
'Context used',
|
|
455
|
-
String((usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)),
|
|
456
|
-
],
|
|
457
|
-
];
|
|
458
|
-
return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [!screenReader ? (_jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(100, width)) })) : null, _jsxs(Text, { children: [' ', tabs.map((tab, index) => (_jsxs(Text, { inverse: index === tabIndex, children: [' ', tab, ' '] }, tab)))] }), _jsx(Text, { children: " " }), rows.map(([label, value]) => (_jsxs(Box, { children: [_jsx(Box, { width: 28, children: _jsxs(Text, { children: [label, ":"] }) }), _jsx(Text, { children: value })] }, label))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "\u2190/\u2192/tab to switch \u00B7 Esc to close" })] }));
|
|
459
|
-
}
|
|
460
425
|
export function ListDashboard({ title, rows, emptyText, selectedIndex, width, screenReader, }) {
|
|
461
426
|
return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [!screenReader ? (_jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(100, width)) })) : null, _jsxs(Text, { bold: true, children: [" ", title] }), _jsx(Text, { children: " " }), rows.length === 0 ? (_jsxs(Text, { dimColor: true, children: [" ", emptyText] })) : (rows.map((row, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { inverse: index === selectedIndex, children: [index === selectedIndex ? '❯ ' : ' ', row.label] }), row.description ? _jsxs(Text, { dimColor: true, children: [" ", row.description] }) : null] }, `${index}-${row.label}`)))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select \u00B7 Esc to close" })] }));
|
|
462
427
|
}
|
|
@@ -603,16 +568,17 @@ export function MentionPicker({ entries, selectedIndex, width, screenReader, })
|
|
|
603
568
|
}
|
|
604
569
|
const SHORTCUT_ROWS = [
|
|
605
570
|
[
|
|
606
|
-
'! for
|
|
571
|
+
'! for bash mode',
|
|
607
572
|
'double tap esc to clear input',
|
|
608
573
|
'ctrl + shift + _ to undo',
|
|
609
574
|
],
|
|
610
|
-
['/ for commands', 'shift + tab to
|
|
575
|
+
['/ for commands', 'shift + tab to auto-accept edits', 'ctrl + z to suspend'],
|
|
611
576
|
[
|
|
612
577
|
'@ for file paths',
|
|
613
578
|
'ctrl + o for verbose output',
|
|
614
579
|
'ctrl + v to paste images',
|
|
615
580
|
],
|
|
581
|
+
['& for background', '', ''],
|
|
616
582
|
[
|
|
617
583
|
'/btw for side question',
|
|
618
584
|
'ctrl + t to toggle tasks',
|
|
@@ -651,16 +617,34 @@ export function SelectionMenu({ title, description, options, selectedIndex, foot
|
|
|
651
617
|
const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), Math.max(0, options.length - maxVisible)));
|
|
652
618
|
const visible = options.slice(start, start + maxVisible);
|
|
653
619
|
const current = options.find((option) => option.selected);
|
|
620
|
+
const maxLabelWidth = options.reduce((max, option, index) => Math.max(max, `${index + 1}. ${option.label}`.length), 0);
|
|
654
621
|
return (_jsxs(Box, { borderStyle: screenReader ? undefined : 'round', borderColor: palette.muted, flexDirection: "column", marginTop: 1, paddingX: screenReader ? 0 : 1, width: screenReader ? undefined : Math.min(80, width), children: [_jsx(Text, { bold: true, children: title }), description ? _jsx(Text, { dimColor: true, children: description }) : null, screenReader && current ? _jsxs(Text, { children: ["Current: ", current.label] }) : null, visible.map((option, visibleIndex) => {
|
|
655
622
|
const index = start + visibleIndex;
|
|
656
623
|
const selected = index === selectedIndex;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
624
|
+
const rowLabel = `${index + 1}. ${option.label}`;
|
|
625
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { ...(!screenReader && selected
|
|
626
|
+
? { color: palette.brand, bold: true }
|
|
627
|
+
: {}), children: [selectionPrefix(selected, screenReader), rowLabel, !screenReader && option.selected ? ' ✔' : '', option.description ? (_jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(2, maxLabelWidth -
|
|
628
|
+
rowLabel.length -
|
|
629
|
+
(option.selected && !screenReader ? 2 : 0))), option.description] })) : null] }) }, `${index}-${option.label}`));
|
|
660
630
|
}), start > 0 || start + visible.length < options.length ? (_jsxs(Text, { dimColor: true, children: [start > 0 ? `↑ ${start} earlier` : '', start > 0 && start + visible.length < options.length ? ' · ' : '', start + visible.length < options.length
|
|
661
631
|
? `↓ ${options.length - start - visible.length} more`
|
|
662
632
|
: ''] })) : null, _jsx(Text, { dimColor: true, children: footer })] }));
|
|
663
633
|
}
|
|
634
|
+
export function ModelMenu({ options, effort, selectedIndex, width, screenReader, }) {
|
|
635
|
+
const palette = useTuiPalette();
|
|
636
|
+
const current = options.find((option) => option.selected);
|
|
637
|
+
const maxLabelWidth = options.reduce((max, option, index) => Math.max(max, `${index + 1}. ${option.label}`.length), 0);
|
|
638
|
+
return (_jsxs(Box, { borderStyle: screenReader ? undefined : 'round', borderColor: palette.muted, flexDirection: "column", marginTop: 1, paddingX: screenReader ? 0 : 1, width: screenReader ? undefined : Math.min(80, width), children: [_jsx(Text, { bold: true, children: "Select model" }), _jsx(Text, { dimColor: true, children: 'Switch between models. Your pick applies to this and future Praxis Code sessions. For other model names, specify with --model.' }), screenReader && current ? _jsxs(Text, { children: ["Current: ", current.label] }) : null, options.map((option, index) => {
|
|
639
|
+
const selected = index === selectedIndex;
|
|
640
|
+
const rowLabel = `${index + 1}. ${option.label}`;
|
|
641
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { ...(!screenReader && selected
|
|
642
|
+
? { color: palette.brand, bold: true }
|
|
643
|
+
: {}), children: [selectionPrefix(selected, screenReader), rowLabel, !screenReader && option.selected ? ' ✔' : '', option.description ? (_jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(2, maxLabelWidth -
|
|
644
|
+
rowLabel.length -
|
|
645
|
+
(option.selected && !screenReader ? 2 : 0))), option.description] })) : null] }) }, `${index}-${option.label}`));
|
|
646
|
+
}), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: palette.accent, children: "\u25CF " }), effort.charAt(0).toUpperCase() + effort.slice(1), " effort (default)", _jsx(Text, { dimColor: true, children: " \u2190/\u2192 to adjust" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to select \u00B7 Esc to cancel" }) })] }));
|
|
647
|
+
}
|
|
664
648
|
function ComposerInput({ input, cursor }) {
|
|
665
649
|
const palette = useTuiPalette();
|
|
666
650
|
const { before, current, after } = composerEditorSegments({
|
|
@@ -701,7 +685,7 @@ export function Composer({ input, cursor, busy, clipboardBusy = false, status, d
|
|
|
701
685
|
? ` · ${usage.cacheReadInputTokens} cached`
|
|
702
686
|
: '', costUsd === undefined ? '' : ` · $${costUsd.toFixed(6)}`] })) : null, _jsx(Text, { dimColor: true, children: line }), clipboardBusy ? (_jsx(Text, { children: "Pasting\u2026" })) : busy ? (_jsxs(Text, { children: [progressBar ? (_jsx(Text, { color: palette.accent, children: reduceMotion ? '•' : SPINNER[spinnerIndex] })) : null, ' ', status, "\u2026 ", _jsx(Text, { dimColor: true, children: "\u00B7 esc to interrupt" })] })) : (_jsxs(Text, { children: [_jsx(Text, { ...(shellMode ? {} : { color: palette.brand }), bold: true, children: shellMode ? '! ' : '❯ ' }), input ? (_jsx(ComposerInput, { cursor: cursor ?? Array.from(input).length, input: input })) : (_jsx(Text, { dimColor: true, children: shellMode
|
|
703
687
|
? 'Enter a shell command'
|
|
704
|
-
: 'Try "review this project"' }))] })), _jsx(Text, { dimColor: true, children: line }), shortcutsVisible ? (_jsx(ShortcutHelp, { width: width })) : (_jsxs(Box, { width: Math.min(100, width), children: [_jsx(Text, { dimColor: true, children: shellMode ? ('! for
|
|
688
|
+
: 'Try "review this project"' }))] })), _jsx(Text, { dimColor: true, children: line }), shortcutsVisible ? (_jsx(ShortcutHelp, { width: width })) : (_jsxs(Box, { width: Math.min(100, width), children: [_jsx(Text, { dimColor: true, children: shellMode ? ('! for bash mode') : (_jsxs(_Fragment, { children: ["\u23F5\u23F5 ", permissionLabel(display.permissionMode), " \u00B7", ' ', busy ? 'esc to interrupt' : '? for shortcuts', " \u00B7 \u2190 for agents", hasThinking
|
|
705
689
|
? ` · ctrl+o ${thinkingExpanded ? 'collapse' : 'expand'}`
|
|
706
690
|
: ''] })) }), _jsx(Box, { flexGrow: 1 }), footerMessage ? (footerMessage.isError ? (_jsx(Text, { color: palette.error, children: footerMessage.text })) : (_jsx(Text, { dimColor: true, children: footerMessage.text }))) : prStatus ? (_jsx(Text, { dimColor: true, children: prStatus })) : turnDuration ? (_jsxs(Text, { dimColor: true, children: ["Cooked for ", turnDuration] })) : display.effort ? (_jsxs(Text, { children: [_jsxs(Text, { color: palette.accent, children: ["\u25CF ", display.effort] }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", editorMode === 'vim' ? 'vim' : '/effort'] })] })) : null] }))] }));
|
|
707
691
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ModelUsage } from '../../core/runtime.js';
|
|
3
3
|
import { type ConfigSettingDefinition, type ConfigSettingsSnapshot, type ConfigValue } from './config-settings.js';
|
|
4
|
-
export type ConfigDashboardTab = '
|
|
4
|
+
export type ConfigDashboardTab = 'status' | 'config' | 'usage';
|
|
5
5
|
export interface ConfigStatusData {
|
|
6
6
|
version: string;
|
|
7
7
|
sessionName?: string;
|
|
@@ -9,6 +9,8 @@ export interface ConfigStatusData {
|
|
|
9
9
|
cwd: string;
|
|
10
10
|
authSource?: string;
|
|
11
11
|
baseUrl?: string;
|
|
12
|
+
proxy?: string;
|
|
13
|
+
mcpSummary?: string;
|
|
12
14
|
model: string;
|
|
13
15
|
settingSources: readonly string[];
|
|
14
16
|
}
|
|
@@ -28,10 +30,6 @@ export interface ConfigContextData {
|
|
|
28
30
|
contextWindowTokens: number;
|
|
29
31
|
categories: readonly ContextCategory[];
|
|
30
32
|
}
|
|
31
|
-
export interface ConfigDashboardValueRow {
|
|
32
|
-
label: string;
|
|
33
|
-
value: string;
|
|
34
|
-
}
|
|
35
33
|
export interface ConfigRow {
|
|
36
34
|
definition: ConfigSettingDefinition;
|
|
37
35
|
value: ConfigValue;
|
|
@@ -45,7 +43,7 @@ export declare function projectContextUsage(data: ConfigContextData): {
|
|
|
45
43
|
freeTokens: number;
|
|
46
44
|
contextWindowTokens: number;
|
|
47
45
|
};
|
|
48
|
-
export declare function ConfigDashboard({ tab, snapshot, query, selectedIndex, searchFocused, status, usage,
|
|
46
|
+
export declare function ConfigDashboard({ tab, snapshot, query, selectedIndex, searchFocused, status, usage, effectiveValues, width, screenReader, maxRows, }: {
|
|
49
47
|
tab: ConfigDashboardTab;
|
|
50
48
|
snapshot: ConfigSettingsSnapshot;
|
|
51
49
|
query?: string;
|
|
@@ -53,8 +51,6 @@ export declare function ConfigDashboard({ tab, snapshot, query, selectedIndex, s
|
|
|
53
51
|
searchFocused?: boolean;
|
|
54
52
|
status?: ConfigStatusData;
|
|
55
53
|
usage?: ConfigUsageData;
|
|
56
|
-
settings?: readonly ConfigDashboardValueRow[];
|
|
57
|
-
stats?: readonly ConfigDashboardValueRow[];
|
|
58
54
|
effectiveValues?: ConfigEffectiveValues;
|
|
59
55
|
width: number;
|
|
60
56
|
screenReader: boolean;
|
|
@@ -2,11 +2,9 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { CLAUDE_2_1_208_CONFIG_SETTINGS, configSettingValue, } from './config-settings.js';
|
|
4
4
|
const tabs = [
|
|
5
|
-
{ id: 'settings', label: 'Settings' },
|
|
6
5
|
{ id: 'status', label: 'Status' },
|
|
7
6
|
{ id: 'config', label: 'Config' },
|
|
8
7
|
{ id: 'usage', label: 'Usage' },
|
|
9
|
-
{ id: 'stats', label: 'Stats' },
|
|
10
8
|
];
|
|
11
9
|
function displayValue(definition, value) {
|
|
12
10
|
if (definition.id === 'permissionMode') {
|
|
@@ -104,7 +102,9 @@ function StatusRows({ status }) {
|
|
|
104
102
|
['cwd', status.cwd],
|
|
105
103
|
...(status.authSource ? [['Auth token', status.authSource]] : []),
|
|
106
104
|
...(status.baseUrl ? [['Provider base URL', status.baseUrl]] : []),
|
|
105
|
+
...(status.proxy ? [['Proxy', status.proxy]] : []),
|
|
107
106
|
['Model', status.model],
|
|
107
|
+
...(status.mcpSummary ? [['MCP servers', status.mcpSummary]] : []),
|
|
108
108
|
['Setting sources', status.settingSources.join(', ')],
|
|
109
109
|
];
|
|
110
110
|
return (_jsxs(_Fragment, { children: [rows.map(([label, value]) => (_jsxs(Box, { children: [_jsx(Box, { width: 22, children: _jsxs(Text, { children: [label, ":"] }) }), _jsx(Text, { children: value })] }, label))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] }));
|
|
@@ -113,37 +113,23 @@ function UsageRows({ usage }) {
|
|
|
113
113
|
const seconds = (milliseconds) => `${Math.round(milliseconds / 1_000)}s`;
|
|
114
114
|
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Session" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Total cost: $", usage.costUsd.toFixed(4)] }), _jsxs(Text, { children: ["Total duration (API): ", seconds(usage.apiDurationMs)] }), _jsxs(Text, { children: ["Total duration (wall): ", seconds(usage.wallDurationMs)] }), _jsxs(Text, { children: ["Total code changes: ", usage.linesAdded, " lines added, ", usage.linesRemoved, ' ', "lines removed"] }), _jsxs(Text, { children: ["Usage: ", usage.usage.inputTokens, " input, ", usage.usage.outputTokens, ' ', "output, ", usage.usage.cacheReadInputTokens ?? 0, " cache read,", ' ', usage.usage.cacheCreationInputTokens ?? 0, " cache write"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] }));
|
|
115
115
|
}
|
|
116
|
-
function
|
|
117
|
-
return rows.map((row) => (_jsxs(Box, { children: [_jsx(Box, { width: 28, children: _jsxs(Text, { children: [row.label, ":"] }) }), _jsx(Text, { children: row.value })] }, row.label)));
|
|
118
|
-
}
|
|
119
|
-
export function ConfigDashboard({ tab, snapshot, query = '', selectedIndex = 0, searchFocused = true, status, usage, settings, stats, effectiveValues, width, screenReader, maxRows = 18, }) {
|
|
116
|
+
export function ConfigDashboard({ tab, snapshot, query = '', selectedIndex = 0, searchFocused = true, status, usage, effectiveValues, width, screenReader, maxRows = 18, }) {
|
|
120
117
|
const rows = projectConfigRows(snapshot, query, effectiveValues);
|
|
121
118
|
let content;
|
|
122
119
|
switch (tab) {
|
|
123
|
-
case 'settings':
|
|
124
|
-
if (settings === undefined)
|
|
125
|
-
throw new Error('Settings tab requires measured settings data');
|
|
126
|
-
content = _jsx(ValueRows, { rows: settings });
|
|
127
|
-
break;
|
|
128
120
|
case 'status':
|
|
129
121
|
if (status === undefined)
|
|
130
122
|
throw new Error('Status tab requires measured status data');
|
|
131
123
|
content = _jsx(StatusRows, { status: status });
|
|
132
124
|
break;
|
|
133
125
|
case 'config':
|
|
134
|
-
content = (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Config" }), _jsx(
|
|
126
|
+
content = (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Config" }), _jsx(ConfigRows, { rows: rows, query: query, selectedIndex: Math.max(0, Math.min(selectedIndex, rows.length - 1)), searchFocused: searchFocused, maxRows: maxRows, screenReader: screenReader, width: width })] }));
|
|
135
127
|
break;
|
|
136
128
|
case 'usage':
|
|
137
129
|
if (usage === undefined)
|
|
138
130
|
throw new Error('Usage tab requires measured usage data');
|
|
139
131
|
content = _jsx(UsageRows, { usage: usage });
|
|
140
132
|
break;
|
|
141
|
-
case 'stats':
|
|
142
|
-
if (stats === undefined)
|
|
143
|
-
throw new Error('Stats tab requires measured stats data');
|
|
144
|
-
content =
|
|
145
|
-
stats.length === 0 ? (_jsx(Text, { children: "No stats available yet. Start using Praxis Code!" })) : (_jsx(ValueRows, { rows: stats }));
|
|
146
|
-
break;
|
|
147
133
|
}
|
|
148
134
|
return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [!screenReader ? _jsx(Text, { children: '─'.repeat(Math.min(100, width)) }) : null, _jsx(DashboardTabs, { active: tab, screenReader: screenReader }), _jsx(Text, { children: " " }), content] }));
|
|
149
135
|
}
|
|
@@ -195,7 +195,7 @@ function withValueAtPath(source, path, value) {
|
|
|
195
195
|
return { ...source, [head]: withValueAtPath(child, tail, value) };
|
|
196
196
|
}
|
|
197
197
|
function validValue(definition, value) {
|
|
198
|
-
if (definition.values === 'language')
|
|
198
|
+
if (definition.values === 'language' || definition.id === 'model')
|
|
199
199
|
return (typeof value === 'string' &&
|
|
200
200
|
value.trim().length > 0 &&
|
|
201
201
|
value.length <= 256);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TranscriptItem, TuiDisplayMetadata } from './claude-style.js';
|
|
2
|
-
export declare function conversationExportText(
|
|
2
|
+
export declare function conversationExportText(_display: TuiDisplayMetadata, items: readonly TranscriptItem[]): string;
|
|
3
3
|
export declare function defaultConversationExportFilename(now?: Date): string;
|
|
4
4
|
export declare function conversationExportPath(cwd: string, filename: string): string;
|
|
5
5
|
export declare function writeConversationExport(path: string, text: string): Promise<void>;
|
|
@@ -4,17 +4,8 @@ function lines(text, prefix) {
|
|
|
4
4
|
const values = text.replace(/\n$/u, '').split('\n');
|
|
5
5
|
return values.map((line, index) => `${index === 0 ? prefix : ' '}${line}`);
|
|
6
6
|
}
|
|
7
|
-
export function conversationExportText(
|
|
8
|
-
const output = [
|
|
9
|
-
`╭─── Praxis Code v${display.version} ───╮`,
|
|
10
|
-
'│ Welcome back!',
|
|
11
|
-
`│ ${display.model ?? 'provider default'} · ${display.effort ?? 'high'} effort`,
|
|
12
|
-
`│ ${display.cwd}`,
|
|
13
|
-
'│ Tips for getting started: Run /help for commands',
|
|
14
|
-
'│ Shared with Claude Code: sessions, memory, skills',
|
|
15
|
-
'╰───',
|
|
16
|
-
'',
|
|
17
|
-
];
|
|
7
|
+
export function conversationExportText(_display, items) {
|
|
8
|
+
const output = [];
|
|
18
9
|
for (const item of items) {
|
|
19
10
|
if (item.kind === 'user')
|
|
20
11
|
output.push('', ...lines(item.text, '❯ '));
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TranscriptItem } from './claude-style.js';
|
|
2
2
|
import type { PraxisRuntimeSettings } from './runtime-settings.js';
|
|
3
3
|
export type CopyCandidate = {
|
|
4
|
+
kind: 'full' | 'code' | 'always';
|
|
4
5
|
label: string;
|
|
5
6
|
description: string;
|
|
6
7
|
text: string;
|
|
8
|
+
filename: string;
|
|
7
9
|
};
|
|
8
10
|
export declare function spinnerTip(settings: Pick<PraxisRuntimeSettings, 'tips'>): string | undefined;
|
|
9
11
|
export declare function questionTimeoutMilliseconds(value: PraxisRuntimeSettings['askUserQuestionTimeout']): number | undefined;
|
|
@@ -32,31 +32,58 @@ export function workflowRuntimeInstructions(settings) {
|
|
|
32
32
|
: `Prefer a ${settings.workflowSizeGuideline} workflow when creating a dynamic workflow unless the user explicitly asks otherwise.`;
|
|
33
33
|
return `Dynamic workflows are available for explicitly requested multi-step automation. ${keyword} ${size}`;
|
|
34
34
|
}
|
|
35
|
+
function fencedCodeBlocks(response) {
|
|
36
|
+
return [...response.matchAll(/```([^\n`]*)\n([\s\S]*?)```/gu)]
|
|
37
|
+
.map((match) => ({
|
|
38
|
+
language: match[1]?.trim() || undefined,
|
|
39
|
+
code: (match[2] ?? '').replace(/\n$/u, ''),
|
|
40
|
+
}))
|
|
41
|
+
.filter((block) => block.code.length > 0);
|
|
42
|
+
}
|
|
43
|
+
function codeFilename(language) {
|
|
44
|
+
const extension = language?.replace(/[^a-zA-Z0-9]/gu, '');
|
|
45
|
+
return `copy.${extension && extension !== 'plaintext' ? extension : 'txt'}`;
|
|
46
|
+
}
|
|
47
|
+
function firstLine(text, maxLength = 60) {
|
|
48
|
+
const line = text.split('\n')[0] ?? '';
|
|
49
|
+
return line.length <= maxLength ? line : `${line.slice(0, maxLength - 1)}…`;
|
|
50
|
+
}
|
|
35
51
|
export function copyCandidates(response) {
|
|
36
|
-
const
|
|
52
|
+
const lines = response.split('\n').length;
|
|
53
|
+
const blocks = fencedCodeBlocks(response);
|
|
54
|
+
return [
|
|
37
55
|
{
|
|
56
|
+
kind: 'full',
|
|
38
57
|
label: 'Full response',
|
|
39
|
-
description:
|
|
58
|
+
description: `${response.length} chars, ${lines} lines`,
|
|
40
59
|
text: response,
|
|
60
|
+
filename: 'response.md',
|
|
61
|
+
},
|
|
62
|
+
...blocks.map((block) => ({
|
|
63
|
+
kind: 'code',
|
|
64
|
+
label: firstLine(block.code),
|
|
65
|
+
description: [
|
|
66
|
+
block.language,
|
|
67
|
+
block.code.includes('\n')
|
|
68
|
+
? `${block.code.split('\n').length} lines`
|
|
69
|
+
: undefined,
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(', '),
|
|
73
|
+
text: block.code,
|
|
74
|
+
filename: codeFilename(block.language),
|
|
75
|
+
})),
|
|
76
|
+
{
|
|
77
|
+
kind: 'always',
|
|
78
|
+
label: 'Always copy full response',
|
|
79
|
+
description: 'Skip this picker in the future (revert via /config)',
|
|
80
|
+
text: response,
|
|
81
|
+
filename: 'response.md',
|
|
41
82
|
},
|
|
42
83
|
];
|
|
43
|
-
const blocks = response.matchAll(/```[^\n]*\n([\s\S]*?)```/gu);
|
|
44
|
-
let index = 0;
|
|
45
|
-
for (const block of blocks) {
|
|
46
|
-
const text = block[1] ?? '';
|
|
47
|
-
if (!text)
|
|
48
|
-
continue;
|
|
49
|
-
index += 1;
|
|
50
|
-
candidates.push({
|
|
51
|
-
label: `Code block ${index}`,
|
|
52
|
-
description: `Copy code block ${index} only.`,
|
|
53
|
-
text,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return candidates;
|
|
57
84
|
}
|
|
58
85
|
export function shouldShowCopyPicker(response) {
|
|
59
|
-
return
|
|
86
|
+
return fencedCodeBlocks(response).length > 0;
|
|
60
87
|
}
|
|
61
88
|
export function externalEditorInitialContent(prompt, history, enabled) {
|
|
62
89
|
if (!enabled)
|