praxis-agent 0.14.0 → 0.16.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.
Files changed (33) hide show
  1. package/README.md +5 -2
  2. package/dist/application/session-service.d.ts +18 -0
  3. package/dist/application/session-service.js +180 -41
  4. package/dist/application/subagent-service.d.ts +7 -1
  5. package/dist/application/subagent-service.js +238 -53
  6. package/dist/cli/interactive.d.ts +7 -1
  7. package/dist/cli/interactive.js +77 -3
  8. package/dist/cli/protocol.d.ts +13 -0
  9. package/dist/cli/protocol.js +34 -0
  10. package/dist/cli/tui/claude-command-inventory.d.ts +1 -2
  11. package/dist/cli/tui/claude-command-inventory.js +1 -6
  12. package/dist/cli/tui/claude-style.d.ts +4 -1
  13. package/dist/cli/tui/claude-style.js +8 -3
  14. package/dist/cli/tui/slash-commands.d.ts +1 -0
  15. package/dist/cli/tui/slash-commands.js +11 -0
  16. package/dist/cli/tui/theme.d.ts +2 -0
  17. package/dist/cli/tui/theme.js +69 -0
  18. package/dist/cli.d.ts +6 -0
  19. package/dist/cli.js +95 -27
  20. package/dist/compatibility/claude/agent-color.d.ts +25 -0
  21. package/dist/compatibility/claude/agent-color.js +79 -0
  22. package/dist/compatibility/claude/fork.js +5 -1
  23. package/dist/compatibility/claude/schema.js +13 -1
  24. package/dist/extensions/claude-extensions.d.ts +19 -1
  25. package/dist/extensions/claude-extensions.js +126 -1
  26. package/dist/hooks/claude-hooks.d.ts +3 -1
  27. package/dist/hooks/claude-hooks.js +14 -0
  28. package/dist/mcp/claude-mcp-tools.d.ts +18 -0
  29. package/dist/mcp/claude-mcp-tools.js +46 -0
  30. package/dist/persistence/claude-transcript-store.js +1 -0
  31. package/dist/tools/claude-interactive-tools.d.ts +1 -0
  32. package/dist/tools/claude-interactive-tools.js +3 -0
  33. package/package.json +2 -1
@@ -17,6 +17,7 @@ import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
17
17
  import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
18
18
  import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
19
19
  import { createRecentlyDeniedStore, } from './tui/recently-denied.js';
20
+ import { agentColorMessage, parseAgentColorInput, } from '../compatibility/claude/agent-color.js';
20
21
  import { filterTuiSlashCommands, mergeTuiSlashCommands, slashCommandQuery, } from './tui/slash-commands.js';
21
22
  import { createComposerEditor, deleteComposerBackward, deleteComposerForward, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursor, moveComposerCursorByWord, } from './tui/composer-editor.js';
22
23
  import { applyMentionReference, fileReferenceAtCursor, filterTuiMentionEntries, loadTuiFileEntries, } from './tui/file-picker.js';
@@ -247,7 +248,7 @@ const HIDDEN_TUI_SLASH_COMMANDS = new Set([
247
248
  'update',
248
249
  'usage',
249
250
  ]);
250
- export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, releaseNotesLoader = (configRoot) => loadClaudeReleaseNotes({ configRoot }), settingSources, }) {
251
+ export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, releaseNotesLoader = (configRoot) => loadClaudeReleaseNotes({ configRoot }), settingSources, }) {
251
252
  const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
252
253
  const width = useTerminalWidth(terminalWidth);
253
254
  const keybindingsRoot = useMemo(() => resolve(keybindingsConfigRoot ??
@@ -304,6 +305,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
304
305
  const statusLineSessionId = useRef(resume?.sessionId ?? randomUUID());
305
306
  const sessionIdRef = useRef(resume?.sessionId ?? null);
306
307
  sessionIdRef.current = sessionId;
308
+ const [sessionColor, setSessionColor] = useState(initialSessionColor);
307
309
  const [sessionName, setSessionName] = useState(null);
308
310
  const [activeSessionSummary, setActiveSessionSummary] = useState(() => resume?.sessionId
309
311
  ? initialSessions.find((session) => session.sessionId === resume.sessionId)
@@ -443,6 +445,17 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
443
445
  const matchingSlashCommands = useMemo(() => commandQuery === null
444
446
  ? []
445
447
  : filterTuiSlashCommands(allSlashCommands, commandQuery), [allSlashCommands, commandQuery]);
448
+ const commandArgumentHint = useMemo(() => {
449
+ if (shellMode || !input.startsWith('/'))
450
+ return undefined;
451
+ if (inputCursor !== input.length)
452
+ return undefined;
453
+ const match = /^\/(\S+) $/u.exec(input);
454
+ if (!match?.[1])
455
+ return undefined;
456
+ const command = allSlashCommands.find((candidate) => candidate.name === match[1]);
457
+ return command?.argumentHint;
458
+ }, [allSlashCommands, input, inputCursor, shellMode]);
446
459
  const commandPaletteVisible = !busy &&
447
460
  !permission &&
448
461
  !planApproval &&
@@ -1443,14 +1456,19 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
1443
1456
  sessionLoadRef.current = loadId;
1444
1457
  if (nextSessionId === null) {
1445
1458
  setHistory([]);
1459
+ setSessionColor(undefined);
1446
1460
  return;
1447
1461
  }
1448
1462
  const loading = (async () => {
1449
1463
  try {
1450
1464
  const commands = await service();
1451
1465
  const transcript = await commands.transcript?.(nextSessionId);
1466
+ const agentColor = commands.agentColor === undefined
1467
+ ? undefined
1468
+ : await commands.agentColor(nextSessionId);
1452
1469
  if (sessionLoadRef.current === loadId) {
1453
1470
  setHistory(transcript ? [...transcript] : []);
1471
+ setSessionColor(agentColor);
1454
1472
  }
1455
1473
  }
1456
1474
  catch (error) {
@@ -1816,6 +1834,32 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
1816
1834
  onTurnChange?.(showing);
1817
1835
  void showing.finally(() => onTurnChange?.(null));
1818
1836
  };
1837
+ const changeSessionColor = (display, selection) => {
1838
+ appendPromptHistory(display);
1839
+ if (selection.kind === 'color')
1840
+ setSessionColor(selection.color);
1841
+ else if (selection.kind === 'reset')
1842
+ setSessionColor(undefined);
1843
+ const changing = (async () => {
1844
+ try {
1845
+ const activeSessionId = await withLocalCommands(async (commands) => {
1846
+ if (!commands.recordColorUsage) {
1847
+ throw new Error('Session color is unavailable.');
1848
+ }
1849
+ return commands.recordColorUsage(sessionId ?? undefined, selection, display, runtimePreferencesRef.current.permissionMode);
1850
+ });
1851
+ if (activeSessionId)
1852
+ setSessionId(activeSessionId);
1853
+ append({ kind: 'user', text: display });
1854
+ append({ kind: 'local-result', text: agentColorMessage(selection) });
1855
+ }
1856
+ catch (error) {
1857
+ warn(error);
1858
+ }
1859
+ })();
1860
+ onTurnChange?.(changing);
1861
+ void changing.finally(() => onTurnChange?.(null));
1862
+ };
1819
1863
  const backgroundSession = () => {
1820
1864
  appendPromptHistory('/background');
1821
1865
  const backgrounding = (async () => {
@@ -4794,6 +4838,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4794
4838
  if (!prompt || prompt === '!')
4795
4839
  return;
4796
4840
  const copyCommand = /^\/copy(?:\s+(\d+))?$/u.exec(prompt);
4841
+ const colorCommand = /^\/color(?:\s+([\s\S]+))?$/u.exec(prompt);
4797
4842
  const sandboxCommand = /^\/sandbox(?:\s+([\s\S]+))?$/u.exec(prompt);
4798
4843
  const tuiCommand = /^\/tui(?:\s+(default|fullscreen))?$/u.exec(prompt);
4799
4844
  const renameCommand = /^\/rename(?:\s+(.+))?$/u.exec(prompt);
@@ -4808,12 +4853,14 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4808
4853
  else if (prompt === '/new') {
4809
4854
  statusLineSessionId.current = randomUUID();
4810
4855
  setSessionId(null);
4856
+ setSessionColor(undefined);
4811
4857
  setPendingFork(false);
4812
4858
  append({ kind: 'notice', text: 'Started a new session.' });
4813
4859
  }
4814
4860
  else if (prompt === '/clear') {
4815
4861
  statusLineSessionId.current = randomUUID();
4816
4862
  setSessionId(null);
4863
+ setSessionColor(undefined);
4817
4864
  setPendingFork(false);
4818
4865
  setHistory([]);
4819
4866
  setUsage(undefined);
@@ -4944,6 +4991,16 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4944
4991
  else if (prompt === '/add-dir') {
4945
4992
  openWorkspaceDirectoryInput();
4946
4993
  }
4994
+ else if (prompt === '/agents') {
4995
+ append({ kind: 'user', text: prompt });
4996
+ append({
4997
+ kind: 'local-result',
4998
+ text: 'The /agents wizard has been removed.\n\nAsk Claude to create or update subagents for you (e.g. "create a code-reviewer subagent that ..."),\nor edit the files directly:\n • .claude/agents/ (this project)\n • ~/.claude/agents/ (all projects)\n\nDocs: https://code.claude.com/docs/en/sub-agents',
4999
+ });
5000
+ }
5001
+ else if (colorCommand) {
5002
+ changeSessionColor(prompt, parseAgentColorInput(colorCommand[1] ?? ''));
5003
+ }
4947
5004
  else if (btwCommand) {
4948
5005
  const sideQuestion = btwCommand[1]?.trim();
4949
5006
  if (sideQuestion)
@@ -5413,7 +5470,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
5413
5470
  label: 'Save to file',
5414
5471
  description: 'Save the conversation to a file in the current directory',
5415
5472
  },
5416
- ], 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
5473
+ ], 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, ...(sessionColor === undefined ? {} : { sessionColor }), ...(commandArgumentHint === undefined
5474
+ ? {}
5475
+ : { commandArgumentHint }), 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
5417
5476
  ? (() => {
5418
5477
  const duration = formatTurnDuration(turnDuration);
5419
5478
  return duration === undefined
@@ -5466,6 +5525,7 @@ export async function runInteractive(options) {
5466
5525
  : controller.signal;
5467
5526
  let currentResume = options.resume;
5468
5527
  let currentInitialPrompt = options.initialPrompt;
5528
+ let initialAgentPromptResolved = false;
5469
5529
  let currentRuntimeSettings = options.runtimeSettings ?? (await loadRuntimeSettings());
5470
5530
  let currentRenderer = currentRuntimeSettings.tui;
5471
5531
  let rendererChange = null;
@@ -5516,12 +5576,26 @@ export async function runInteractive(options) {
5516
5576
  const resume = canonicalResumeSession
5517
5577
  ? { ...currentResume, sessionId: canonicalResumeSession.sessionId }
5518
5578
  : currentResume;
5579
+ if (!initialAgentPromptResolved) {
5580
+ const agentInitialPrompt = resume === undefined ? listing.initialAgentPrompt?.() : undefined;
5581
+ if (agentInitialPrompt) {
5582
+ currentInitialPrompt = currentInitialPrompt
5583
+ ? `${agentInitialPrompt}\n\n${currentInitialPrompt}`
5584
+ : agentInitialPrompt;
5585
+ }
5586
+ initialAgentPromptResolved = true;
5587
+ }
5519
5588
  let initialHistory = [];
5589
+ let initialSessionColor;
5520
5590
  try {
5521
5591
  initialHistory =
5522
5592
  resume?.sessionId === undefined || listing.transcript === undefined
5523
5593
  ? []
5524
5594
  : await listing.transcript(resume.sessionId);
5595
+ initialSessionColor =
5596
+ resume?.sessionId === undefined || listing.agentColor === undefined
5597
+ ? undefined
5598
+ : await listing.agentColor(resume.sessionId);
5525
5599
  }
5526
5600
  catch (error) {
5527
5601
  try {
@@ -5546,7 +5620,7 @@ export async function runInteractive(options) {
5546
5620
  let cleanup = null;
5547
5621
  let backgrounded;
5548
5622
  rendererChange = null;
5549
- const instance = render(_jsx(InteractiveApp, { factory: options.factory, initialSessions: initialSessions, slashCommands: initialSlashCommands, agents: initialAgents, initialHistory: history, runtimeSettings: currentRuntimeSettings, ...(options.settingSources === undefined
5623
+ const instance = render(_jsx(InteractiveApp, { factory: options.factory, initialSessions: initialSessions, slashCommands: initialSlashCommands, agents: initialAgents, initialHistory: history, ...(initialSessionColor === undefined ? {} : { initialSessionColor }), runtimeSettings: currentRuntimeSettings, ...(options.settingSources === undefined
5550
5624
  ? {}
5551
5625
  : { settingSources: options.settingSources }), initialThemeSettings: initialThemeSettings, ...(initialThemeLoadError === undefined
5552
5626
  ? {}
@@ -206,6 +206,13 @@ export interface ProtocolResult {
206
206
  }
207
207
  export declare function createSuccessResult(result: ProtocolResult, info: CliRuntimeInfo, startedAt: number, modelTurns: number): Record<string, unknown>;
208
208
  export declare function createErrorResult(message: string, sessionId: string, startedAt: number, modelTurns: number): Record<string, unknown>;
209
+ /**
210
+ * Returns the argument text of a headless `/color` prompt, or undefined when
211
+ * the prompt is not a bare `/color` command. A trailing space matches with an
212
+ * empty argument (random color), while `/colorblue` and `/colorful` are not
213
+ * the command.
214
+ */
215
+ export declare function matchHeadlessColorCommand(prompt: string): string | undefined;
209
216
  export declare function parseCliInvocation(argv: readonly string[]): CliInvocation;
210
217
  export declare function readStreamJsonMessages(input: AsyncIterable<string | Uint8Array>): AsyncGenerator<StreamJsonMessage>;
211
218
  export declare function readStreamUserMessages(input: AsyncIterable<string | Uint8Array>): AsyncGenerator<StreamUserMessage>;
@@ -232,6 +239,12 @@ export declare class StreamJsonOutput {
232
239
  constructor(write: (value: unknown) => void, info: CliRuntimeInfo, sessionId: string, includePartialMessages: boolean, includeHookEvents?: boolean);
233
240
  init(): void;
234
241
  replayUser(message: StreamUserMessage['message']): void;
242
+ /**
243
+ * Emits the synthetic assistant message for a provider-free local command
244
+ * (e.g. /color). It must not touch the turn state, so the following result
245
+ * keeps num_turns 0 and no stream_event records are produced.
246
+ */
247
+ syntheticAssistant(text: string): void;
235
248
  controlRequest(request: {
236
249
  request_id: string;
237
250
  request: Record<string, unknown>;
@@ -80,6 +80,18 @@ export function createErrorResult(message, sessionId, startedAt, modelTurns) {
80
80
  uuid: randomUUID(),
81
81
  };
82
82
  }
83
+ /**
84
+ * Returns the argument text of a headless `/color` prompt, or undefined when
85
+ * the prompt is not a bare `/color` command. A trailing space matches with an
86
+ * empty argument (random color), while `/colorblue` and `/colorful` are not
87
+ * the command.
88
+ */
89
+ export function matchHeadlessColorCommand(prompt) {
90
+ const match = /^\/color(?:\s+([\s\S]*))?$/u.exec(prompt);
91
+ if (match === null)
92
+ return undefined;
93
+ return match[1] ?? '';
94
+ }
83
95
  const INPUT_FORMATS = ['text', 'stream-json'];
84
96
  const OUTPUT_FORMATS = ['text', 'json', 'stream-json'];
85
97
  const PERMISSION_MODES = [
@@ -1594,6 +1606,28 @@ export class StreamJsonOutput {
1594
1606
  replayUser(message) {
1595
1607
  this.write({ type: 'user', message, session_id: this.sessionId });
1596
1608
  }
1609
+ /**
1610
+ * Emits the synthetic assistant message for a provider-free local command
1611
+ * (e.g. /color). It must not touch the turn state, so the following result
1612
+ * keeps num_turns 0 and no stream_event records are produced.
1613
+ */
1614
+ syntheticAssistant(text) {
1615
+ this.write({
1616
+ type: 'assistant',
1617
+ message: {
1618
+ id: randomUUID(),
1619
+ type: 'message',
1620
+ role: 'assistant',
1621
+ model: '<synthetic>',
1622
+ content: [{ type: 'text', text }],
1623
+ stop_reason: 'stop_sequence',
1624
+ stop_sequence: '',
1625
+ usage: { input_tokens: 0, output_tokens: 0 },
1626
+ },
1627
+ parent_tool_use_id: null,
1628
+ session_id: this.sessionId,
1629
+ });
1630
+ }
1597
1631
  controlRequest(request) {
1598
1632
  this.write({ type: 'control_request', ...request });
1599
1633
  }
@@ -38,9 +38,8 @@ export declare const CLAUDE_2_1_208_COMMAND_INVENTORY: readonly [{
38
38
  readonly visibility: "visible";
39
39
  }, {
40
40
  readonly name: "color";
41
- readonly disposition: "deferred";
41
+ readonly disposition: "included";
42
42
  readonly visibility: "visible";
43
- readonly reason: "The dedicated /color contract is required; /theme is not a substitute.";
44
43
  }, {
45
44
  readonly name: "compact";
46
45
  readonly disposition: "included";
@@ -20,12 +20,7 @@ export const CLAUDE_2_1_208_COMMAND_INVENTORY = [
20
20
  reason: 'The CLI-driven Chrome integration is required parity work and is not implemented yet.',
21
21
  },
22
22
  { name: 'clear', disposition: 'included', visibility: 'visible' },
23
- {
24
- name: 'color',
25
- disposition: 'deferred',
26
- visibility: 'visible',
27
- reason: 'The dedicated /color contract is required; /theme is not a substitute.',
28
- },
23
+ { name: 'color', disposition: 'included', visibility: 'visible' },
29
24
  { name: 'compact', disposition: 'included', visibility: 'visible' },
30
25
  { name: 'config', disposition: 'included', visibility: 'visible' },
31
26
  { name: 'copy', disposition: 'included', visibility: 'visible' },
@@ -1,4 +1,5 @@
1
1
  import type { ModelToolCall, ModelUsage } from '../../core/runtime.js';
2
+ import type { AgentColorName } from '../../compatibility/claude/agent-color.js';
2
3
  import type { TuiFileEntry, TuiMentionEntry } from './file-picker.js';
3
4
  import { type TuiDiffSnapshot } from './git-diff.js';
4
5
  import { type TuiHookConfiguration } from './hook-settings.js';
@@ -230,7 +231,7 @@ export declare function ModelMenu({ options, effort, selectedIndex, width, scree
230
231
  export declare function ExternalEditorWait({ screenReader, }: {
231
232
  screenReader: boolean;
232
233
  }): import("react").JSX.Element;
233
- export declare function Composer({ input, cursor, busy, clipboardBusy, status, display, usage, costUsd, width, screenReader, hasThinking, thinkingExpanded, shortcutsVisible, shellMode, footerMessage, reduceMotion, progressBar, turnDuration, editorMode, prStatus, }: {
234
+ export declare function Composer({ input, cursor, busy, clipboardBusy, status, display, usage, costUsd, width, screenReader, hasThinking, thinkingExpanded, shortcutsVisible, shellMode, footerMessage, reduceMotion, progressBar, turnDuration, editorMode, prStatus, sessionColor, commandArgumentHint, }: {
234
235
  input: string;
235
236
  cursor?: number;
236
237
  busy: boolean;
@@ -254,6 +255,8 @@ export declare function Composer({ input, cursor, busy, clipboardBusy, status, d
254
255
  turnDuration?: string;
255
256
  editorMode?: 'normal' | 'vim';
256
257
  prStatus?: string;
258
+ sessionColor?: AgentColorName;
259
+ commandArgumentHint?: string;
257
260
  }): import("react").JSX.Element;
258
261
  export declare function DialogFrame({ title, children, screenReader, }: {
259
262
  title: string;
@@ -659,7 +659,7 @@ export function ExternalEditorWait({ screenReader, }) {
659
659
  return _jsx(Text, { children: "External editor open. Save and close it to continue." });
660
660
  return (_jsxs(Box, { flexDirection: "column", children: [_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\u2500" }), _jsx(Text, { children: "Save and close editor to continue..." }), _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\u2500" })] }));
661
661
  }
662
- export function Composer({ input, cursor, busy, clipboardBusy = false, status, display, usage, costUsd, width, screenReader, hasThinking = false, thinkingExpanded = false, shortcutsVisible = false, shellMode = false, footerMessage, reduceMotion = false, progressBar = true, turnDuration, editorMode = 'normal', prStatus, }) {
662
+ export function Composer({ input, cursor, busy, clipboardBusy = false, status, display, usage, costUsd, width, screenReader, hasThinking = false, thinkingExpanded = false, shortcutsVisible = false, shellMode = false, footerMessage, reduceMotion = false, progressBar = true, turnDuration, editorMode = 'normal', prStatus, sessionColor, commandArgumentHint, }) {
663
663
  const palette = useTuiPalette();
664
664
  const [spinnerIndex, setSpinnerIndex] = useState(0);
665
665
  useEffect(() => {
@@ -677,15 +677,20 @@ export function Composer({ input, cursor, busy, clipboardBusy = false, status, d
677
677
  ? `Shell command: ${input}`
678
678
  : `Prompt: ${input}` }));
679
679
  const line = '─'.repeat(Math.max(12, Math.min(100, width)));
680
+ const separatorColor = sessionColor === undefined ? undefined : palette.sessionColors[sessionColor];
680
681
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [usage ? (_jsxs(Text, { dimColor: true, children: ["Context \u00B7 ", usage.inputTokens + usage.outputTokens, " tokens", display.contextWindowTokens
681
682
  ? ` / ${display.contextWindowTokens} (${Math.min(100, Math.round(((usage.inputTokens + usage.outputTokens) /
682
683
  display.contextWindowTokens) *
683
684
  100))}%)`
684
685
  : '', usage.cacheReadInputTokens
685
686
  ? ` · ${usage.cacheReadInputTokens} cached`
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
687
+ : '', costUsd === undefined ? '' : ` · $${costUsd.toFixed(6)}`] })) : null, _jsx(Text, { ...(separatorColor === undefined
688
+ ? { dimColor: true }
689
+ : { color: separatorColor }), 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 ? (_jsxs(Text, { children: [_jsx(ComposerInput, { cursor: cursor ?? Array.from(input).length, input: input }), commandArgumentHint ? (_jsxs(Text, { dimColor: true, children: [input.endsWith(' ') ? '' : ' ', commandArgumentHint] })) : null] })) : (_jsx(Text, { dimColor: true, children: shellMode
687
690
  ? 'Enter a shell command'
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
691
+ : 'Try "review this project"' }))] })), _jsx(Text, { ...(separatorColor === undefined
692
+ ? { dimColor: true }
693
+ : { color: separatorColor }), 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
689
694
  ? ` · ctrl+o ${thinkingExpanded ? 'collapse' : 'expand'}`
690
695
  : ''] })) }), _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] }))] }));
691
696
  }
@@ -3,6 +3,7 @@ export interface TuiSlashCommand {
3
3
  name: string;
4
4
  description: string;
5
5
  source: TuiSlashCommandSource;
6
+ argumentHint?: string;
6
7
  progressMessage?: string;
7
8
  }
8
9
  export declare const BUILTIN_TUI_SLASH_COMMANDS: readonly TuiSlashCommand[];
@@ -4,6 +4,11 @@ export const BUILTIN_TUI_SLASH_COMMANDS = [
4
4
  description: 'Add a new working directory',
5
5
  source: 'builtin',
6
6
  },
7
+ {
8
+ name: 'agents',
9
+ description: '(removed) Ask Claude to create/manage subagents, or edit .claude/agents/',
10
+ source: 'builtin',
11
+ },
7
12
  {
8
13
  name: 'branch',
9
14
  description: 'Create a branch of the current conversation at this point',
@@ -29,6 +34,12 @@ export const BUILTIN_TUI_SLASH_COMMANDS = [
29
34
  description: 'Start a new session with empty context; previous session stays on disk (resumable with /resume)',
30
35
  source: 'builtin',
31
36
  },
37
+ {
38
+ name: 'color',
39
+ description: 'Set the prompt bar color for this session',
40
+ argumentHint: '[red|blue|green|yellow|purple|orange|pink|cyan|default]',
41
+ source: 'builtin',
42
+ },
32
43
  {
33
44
  name: 'compact',
34
45
  description: 'Clear conversation history but keep a summary in context',
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
+ import type { AgentColorName } from '../../compatibility/claude/agent-color.js';
2
3
  import type { TuiCustomTheme } from './custom-themes.js';
3
4
  export declare const TUI_THEMES: readonly ["auto", "dark", "light", "dark-daltonized", "light-daltonized", "dark-ansi", "light-ansi"];
4
5
  export type TuiTheme = (typeof TUI_THEMES)[number];
@@ -31,6 +32,7 @@ export interface TuiPalette {
31
32
  warning: string;
32
33
  muted: string;
33
34
  selectionText: string;
35
+ sessionColors: Readonly<Record<AgentColorName, string>>;
34
36
  syntaxTheme: 'Monokai Extended' | 'GitHub' | 'ansi';
35
37
  syntax: TuiSyntaxPalette;
36
38
  }
@@ -44,6 +44,56 @@ const LIGHT_SYNTAX = {
44
44
  addedBackground: '#d7ffd7',
45
45
  addedHighlight: '#afffaf',
46
46
  };
47
+ const NORMAL_SESSION_COLORS = {
48
+ red: '#dc2626',
49
+ blue: '#2563eb',
50
+ green: '#16a34a',
51
+ yellow: '#ca8a04',
52
+ purple: '#9333ea',
53
+ orange: '#ea580c',
54
+ pink: '#db2777',
55
+ cyan: '#0891b2',
56
+ };
57
+ const LIGHT_ANSI_SESSION_COLORS = {
58
+ red: 'red',
59
+ blue: 'blue',
60
+ green: 'green',
61
+ yellow: 'yellow',
62
+ purple: 'magenta',
63
+ orange: 'redBright',
64
+ pink: 'magentaBright',
65
+ cyan: 'cyan',
66
+ };
67
+ const DARK_ANSI_SESSION_COLORS = {
68
+ red: 'redBright',
69
+ blue: 'blueBright',
70
+ green: 'greenBright',
71
+ yellow: 'yellowBright',
72
+ purple: 'magentaBright',
73
+ orange: 'redBright',
74
+ pink: 'magentaBright',
75
+ cyan: 'cyanBright',
76
+ };
77
+ const LIGHT_DALTONIZED_SESSION_COLORS = {
78
+ red: '#cc0000',
79
+ blue: '#0066cc',
80
+ green: '#00cc00',
81
+ yellow: '#ffcc00',
82
+ purple: '#800080',
83
+ orange: '#ff8000',
84
+ pink: '#ff66b2',
85
+ cyan: '#00b2b2',
86
+ };
87
+ const DARK_DALTONIZED_SESSION_COLORS = {
88
+ red: '#ff6666',
89
+ blue: '#66b2ff',
90
+ green: '#66ff66',
91
+ yellow: '#ffff66',
92
+ purple: '#b266ff',
93
+ orange: '#ffb266',
94
+ pink: '#ff99cc',
95
+ cyan: '#66cccc',
96
+ };
47
97
  function automaticDark(environment) {
48
98
  const background = environment.COLORFGBG?.split(';').at(-1);
49
99
  return background === undefined || Number(background) < 8;
@@ -195,11 +245,30 @@ export function tuiPalette(profile, syntaxHighlightingDisabled = false, environm
195
245
  ? adaptSyntaxColors(syntaxBase, adaptAutoColor)
196
246
  : syntaxBase;
197
247
  const autoColor = (color) => profile === 'auto' ? adaptAutoColor(color) : color;
248
+ const sessionColors = ansiOnly
249
+ ? dark
250
+ ? DARK_ANSI_SESSION_COLORS
251
+ : LIGHT_ANSI_SESSION_COLORS
252
+ : daltonized
253
+ ? dark
254
+ ? DARK_DALTONIZED_SESSION_COLORS
255
+ : LIGHT_DALTONIZED_SESSION_COLORS
256
+ : {
257
+ red: autoColor(NORMAL_SESSION_COLORS.red),
258
+ blue: autoColor(NORMAL_SESSION_COLORS.blue),
259
+ green: autoColor(NORMAL_SESSION_COLORS.green),
260
+ yellow: autoColor(NORMAL_SESSION_COLORS.yellow),
261
+ purple: autoColor(NORMAL_SESSION_COLORS.purple),
262
+ orange: autoColor(NORMAL_SESSION_COLORS.orange),
263
+ pink: autoColor(NORMAL_SESSION_COLORS.pink),
264
+ cyan: autoColor(NORMAL_SESSION_COLORS.cyan),
265
+ };
198
266
  const palette = {
199
267
  profile,
200
268
  dark,
201
269
  ansiOnly,
202
270
  syntaxHighlightingDisabled,
271
+ sessionColors,
203
272
  brand: ansiOnly ? 'redBright' : autoColor('#D97757'),
204
273
  accent: ansiOnly
205
274
  ? 'magentaBright'
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { type ForkResult, type ManualCompactResult, type ManualCompactSelection, type RewindPoint, type SessionForkCheckpoint, type SessionInspection, type SessionRunResult, type SessionSummary, type SideQuestionForkResult, type SideQuestionResult } from './application/session-service.js';
3
+ import { type AgentColorName, type AgentColorSelection } from './compatibility/claude/agent-color.js';
3
4
  import type { ClaudeDisplayTranscriptItem } from './compatibility/claude/projection.js';
4
5
  import { type ModelDocument, type ModelImage, type ModelProvider, type ModelToolCall, type PermissionApproval, type PermissionDecision, type ToolRegistry, type RuntimeEventSink } from './core/runtime.js';
5
6
  import type { InteractiveResumeOptions, InteractiveServiceFactory } from './cli/interactive.js';
@@ -40,6 +41,10 @@ interface SessionCommands {
40
41
  retryRecentlyDenied?(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
41
42
  answerSideQuestion?(sessionId: string | undefined, question: string, signal?: AbortSignal, onDelta?: (delta: string) => void, permissionMode?: ClaudePermissionMode): Promise<SideQuestionResult>;
42
43
  recordBtwUsage?(sessionId: string | undefined, permissionMode?: ClaudePermissionMode): Promise<string>;
44
+ recordColorUsage?(sessionId: string | undefined, selection: AgentColorSelection, display: string, permissionMode?: ClaudePermissionMode, options?: {
45
+ createSession?: boolean;
46
+ }): Promise<string>;
47
+ agentColor?(sessionId: string): Promise<AgentColorName | undefined>;
43
48
  recordBackgroundUsage?(sessionId: string | undefined, permissionMode?: ClaudePermissionMode): Promise<string>;
44
49
  recordBackgroundLaunch?(sessionId: string): Promise<SessionForkCheckpoint>;
45
50
  forkSideQuestion?(sessionId: string, question: string, signal?: AbortSignal): Promise<SideQuestionForkResult>;
@@ -67,6 +72,7 @@ interface SessionCommands {
67
72
  mcpTools?(name: string): Promise<readonly ClaudeMcpToolInspection[]>;
68
73
  close?(): Promise<void>;
69
74
  runtimeInfo?(): CliRuntimeInfo;
75
+ initialAgentPrompt?(): string | undefined;
70
76
  agentDefinitions?(): readonly {
71
77
  name: string;
72
78
  description: string;