praxis-agent 0.9.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 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`, `/usage`, `/mcp`, `/memory` shared
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,
@@ -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 { homedir } from 'node:os';
4
- import { resolve } from 'node:path';
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, StatusDashboard, ThemePicker, CustomThemeEditor, Transcript, WelcomePanel, useTerminalWidth, } from './tui/claude-style.js';
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, shouldShowCopyPicker, spinnerTip, } from './tui/runtime-interactions.js';
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
- label: runtimeDisplay.model
444
- ? `Current: ${runtimeDisplay.model}`
445
- : 'Current: provider default',
446
- description: 'Keep the model selected for this interactive session.',
447
- selected: true,
448
- },
449
- {
450
- label: 'Use invocation default',
451
- description: 'Use the model supplied by the original CLI invocation.',
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
- ], [runtimeDisplay.model]);
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
- if (!runtimeSettingsRef.current.copyFullResponse &&
1439
- shouldShowCopyPicker(response.text)) {
1440
- updateMenu({
1441
- kind: 'copy',
1442
- candidates: copyCandidates(response.text),
1443
- selectedIndex: 0,
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
- const copying = clipboardWriter(response.text).then(() => append({
1448
- kind: 'local-result',
1449
- text: `Copied ${position === 1 ? 'last' : `${ordinal(position)}-latest`} response to clipboard.`,
1450
- }), (error) => warn(error));
1451
- onTurnChange?.(copying);
1452
- void copying.finally(() => onTurnChange?.(null));
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 && lower !== 's')
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
- else {
4055
- updateMenu(null);
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 snapshot = await loadConfigSettings(configTarget);
4481
- if (menuRef.current?.kind === 'config')
4482
- updateMenu({ ...menuRef.current, snapshot });
4483
- }
4484
- catch (error) {
4485
- warn(error);
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
- finally {
4488
- setBusy(false);
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
- updateMenu({ kind: 'status', tabIndex: 3 });
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 === 'status' ? (_jsx(StatusDashboard, { tabIndex: menu.tabIndex, version: runtimeDisplay.version, sessionId: sessionId, display: runtimeDisplay, ...(usage === undefined ? {} : { usage }), ...(costUsd === undefined ? {} : { costUsd }), turnCount: turnNumberRef.current, toolCount: history.filter((item) => item.kind === 'tool').length, commandCount: allSlashCommands.length, detailedTranscript: thinkingExpanded, width: width, screenReader: axScreenReader })) : menu.kind === 'copy' ? (_jsx(SelectionMenu, { title: "Copy response", description: "Choose the response content to copy.", options: menu.candidates.map((candidate) => ({
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, settings: [
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: ['user', 'project', 'local'],
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
- }, stats: [], 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(SelectionMenu, { title: "Select model", description: `Effort: ${runtimePreferences.effort}`, options: modelOptions, selectedIndex: menu.selectedIndex, footer: "\u2191/\u2193 select \u00B7 \u2190/\u2192 effort \u00B7 Enter applies to this session \u00B7 Esc cancels", 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) => ({
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,8 +46,8 @@ 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();
@@ -59,7 +59,7 @@ export function WelcomePanel({ display, width, }) {
59
59
  // Title block = "╭" + "───" + "Praxis Code vX.Y.Z" + " " = 4 + title.length + 1 chars.
60
60
  const title = `${brand} Code v${display.version}`;
61
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 /help for commands" }), _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: "Shared with Claude Code" }), _jsx(Text, { children: "Sessions, memory, skills" })] })] }) })] }));
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" })] })] }) })] }));
63
63
  }
64
64
  function InlineText({ text }) {
65
65
  const palette = useTuiPalette();
@@ -183,19 +183,27 @@ function ThinkingBlock({ text, active, expanded, screenReader, }) {
183
183
  const showFull = screenReader || active || expanded;
184
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] }));
185
185
  }
186
- function ContextUsageBlock({ usedTokens, contextWindowTokens, skills, screenReader, }) {
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, }) {
187
197
  const totalTokens = Math.max(1, contextWindowTokens);
188
198
  const compactBuffer = Math.round(totalTokens * 0.165);
189
199
  const usable = Math.max(1, totalTokens - compactBuffer);
190
200
  if (screenReader) {
191
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Context Usage" }), _jsxs(Text, { children: [usedTokens.toLocaleString(), " of ", totalTokens.toLocaleString(), " tokens"] }), _jsxs(Text, { children: ["Autocompact buffer: ", compactBuffer.toLocaleString(), " tokens"] }), _jsxs(Text, { children: ["Skills: ", skills.map(({ name }) => name).join(', ') || 'none'] })] }));
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'] })] }));
192
202
  }
193
- const usedCells = Math.min(100, Math.ceil((usedTokens / totalTokens) * 100));
194
- const bufferCells = Math.min(100 - usedCells, Math.ceil((compactBuffer / totalTokens) * 100));
195
- const cells = Array.from({ length: 100 }, (_, index) => index < usedCells ? '⛁' : index >= 100 - bufferCells ? '⛝' : '⛶');
196
- 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: 10 }, (_, row) => (_jsx(Text, { children: cells.slice(row * 10, row * 10 + 10).join(' ') }, row))) }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [usedTokens.toLocaleString(), "/", totalTokens.toLocaleString(), " tokens"] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Estimated usage by category" }), _jsxs(Text, { children: ["\u26C1 Skills:", ' ', skills
197
- .reduce((total, skill) => total + skill.tokens, 0)
198
- .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))))] }));
199
207
  }
200
208
  export function MarkdownText({ text }) {
201
209
  const lines = text.split('\n');
@@ -403,7 +411,7 @@ export function ThemePicker({ currentTheme, selectedIndex, syntaxHighlightingDis
403
411
  ? {}
404
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
405
413
  ? {}
406
- : { backgroundColor: syntax.addedHighlight }), children: "Praxis" }), _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__'
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__'
407
415
  ? 'Enter to create a custom theme'
408
416
  : syntaxHighlightingDisabled
409
417
  ? 'Syntax highlighting disabled (ctrl+t to enable)'
@@ -414,55 +422,6 @@ export function CustomThemeEditor({ theme, token, value, tokens = [], selectedIn
414
422
  .slice(selectedIndex, selectedIndex + 8)
415
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] }));
416
424
  }
417
- export function StatusDashboard({ tabIndex, version, sessionId, display, usage, costUsd, turnCount, toolCount, commandCount, detailedTranscript, width, screenReader, }) {
418
- const tabs = ['Settings', 'Status', 'Config', 'Usage', 'Stats'];
419
- const rows = tabIndex === 0
420
- ? [
421
- ['Thinking mode', 'provider controlled'],
422
- ['Verbose output', detailedTranscript ? 'true' : 'false'],
423
- ['Default permission mode', permissionLabel(display.permissionMode)],
424
- ['Context compaction', 'automatic'],
425
- ['Shared Claude data', 'enabled'],
426
- ]
427
- : tabIndex === 1
428
- ? [
429
- ['Version', version],
430
- ['Session ID', sessionId ?? 'new session'],
431
- ['cwd', display.cwd],
432
- ['Model', display.model ?? 'provider default'],
433
- ['Permission mode', permissionLabel(display.permissionMode)],
434
- ]
435
- : tabIndex === 2
436
- ? [
437
- ['Model', display.model ?? 'provider default'],
438
- ['Effort', display.effort ?? 'high'],
439
- [
440
- 'Context window',
441
- String(display.contextWindowTokens ?? 'provider default'),
442
- ],
443
- ['Available commands', String(commandCount)],
444
- ]
445
- : tabIndex === 3
446
- ? [
447
- ['Input tokens', String(usage?.inputTokens ?? 0)],
448
- ['Output tokens', String(usage?.outputTokens ?? 0)],
449
- [
450
- 'Session cost',
451
- costUsd === undefined
452
- ? 'unavailable'
453
- : `$${costUsd.toFixed(4)}`,
454
- ],
455
- ]
456
- : [
457
- ['Turns', String(turnCount)],
458
- ['Tool calls', String(toolCount)],
459
- [
460
- 'Context used',
461
- String((usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)),
462
- ],
463
- ];
464
- 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" })] }));
465
- }
466
425
  export function ListDashboard({ title, rows, emptyText, selectedIndex, width, screenReader, }) {
467
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" })] }));
468
427
  }
@@ -609,16 +568,17 @@ export function MentionPicker({ entries, selectedIndex, width, screenReader, })
609
568
  }
610
569
  const SHORTCUT_ROWS = [
611
570
  [
612
- '! for shell mode',
571
+ '! for bash mode',
613
572
  'double tap esc to clear input',
614
573
  'ctrl + shift + _ to undo',
615
574
  ],
616
- ['/ for commands', 'shift + tab to cycle permissions', 'ctrl + z to suspend'],
575
+ ['/ for commands', 'shift + tab to auto-accept edits', 'ctrl + z to suspend'],
617
576
  [
618
577
  '@ for file paths',
619
578
  'ctrl + o for verbose output',
620
579
  'ctrl + v to paste images',
621
580
  ],
581
+ ['& for background', '', ''],
622
582
  [
623
583
  '/btw for side question',
624
584
  'ctrl + t to toggle tasks',
@@ -657,16 +617,34 @@ export function SelectionMenu({ title, description, options, selectedIndex, foot
657
617
  const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), Math.max(0, options.length - maxVisible)));
658
618
  const visible = options.slice(start, start + maxVisible);
659
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);
660
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) => {
661
622
  const index = start + visibleIndex;
662
623
  const selected = index === selectedIndex;
663
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { ...(!screenReader && selected
664
- ? { color: palette.brand, bold: true }
665
- : {}), children: [selectionPrefix(selected, screenReader), index + 1, ". ", option.label, !screenReader && option.selected ? ' ✔' : ''] }), option.description ? (_jsxs(Text, { dimColor: true, children: [" ", option.description] })) : null] }, `${index}-${option.label}`));
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}`));
666
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
667
631
  ? `↓ ${options.length - start - visible.length} more`
668
632
  : ''] })) : null, _jsx(Text, { dimColor: true, children: footer })] }));
669
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
+ }
670
648
  function ComposerInput({ input, cursor }) {
671
649
  const palette = useTuiPalette();
672
650
  const { before, current, after } = composerEditorSegments({
@@ -707,7 +685,7 @@ export function Composer({ input, cursor, busy, clipboardBusy = false, status, d
707
685
  ? ` · ${usage.cacheReadInputTokens} cached`
708
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
709
687
  ? 'Enter a shell command'
710
- : '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 shell mode') : (_jsxs(_Fragment, { children: ["\u23F5\u23F5 ", permissionLabel(display.permissionMode), " \u00B7", ' ', busy ? 'esc to interrupt' : '? for shortcuts', " \u00B7 \u2190 for agents", hasThinking
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
711
689
  ? ` · ctrl+o ${thinkingExpanded ? 'collapse' : 'expand'}`
712
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] }))] }));
713
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 = 'settings' | 'status' | 'config' | 'usage' | 'stats';
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, settings, stats, effectiveValues, width, screenReader, maxRows, }: {
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 ValueRows({ rows }) {
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(Text, { children: "Available commands: /config \u00B7 /status \u00B7 /usage" }), _jsx(ConfigRows, { rows: rows, query: query, selectedIndex: Math.max(0, Math.min(selectedIndex, rows.length - 1)), searchFocused: searchFocused, maxRows: maxRows, screenReader: screenReader, width: width })] }));
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(display: TuiDisplayMetadata, items: readonly TranscriptItem[]): string;
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(display, items) {
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 candidates = [
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: 'Copy the complete assistant response.',
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 copyCandidates(response).length > 1;
86
+ return fencedCodeBlocks(response).length > 0;
60
87
  }
61
88
  export function externalEditorInitialContent(prompt, history, enabled) {
62
89
  if (!enabled)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",