praxis-agent 0.15.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.
package/README.md CHANGED
@@ -95,7 +95,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
95
95
  tabs, `/sandbox` mode/dependency/override/config controls, local cached
96
96
  `/release-notes`, Claude-compatible `/statusline` command execution and setup
97
97
  agent, source-aligned `/init` project-instruction onboarding with its enhanced
98
- skills/hooks flow, `/mcp`, `/memory` shared instruction and auto-memory
98
+ skills/hooks flow, provider-free per-session `/color` prompt-bar styling,
99
+ `/mcp`, `/memory` shared instruction and auto-memory
99
100
  access, and live extension-reload controls,
100
101
  cursor/history composer, per-session model/effort/permission controls,
101
102
  context/status/skill/task dashboards, prompt stash and continuation shortcuts,
@@ -1,3 +1,4 @@
1
+ import { type AgentColorName, type AgentColorSelection } from '../compatibility/claude/agent-color.js';
1
2
  import type { ClaudeConditionalRuleResolver } from '../compatibility/claude/context.js';
2
3
  import { type ClaudeFileResource, type ClaudeFileResourceConfig } from '../compatibility/claude/file-resources.js';
3
4
  import { type ClaudeDisplayTranscriptItem } from '../compatibility/claude/projection.js';
@@ -182,6 +183,7 @@ export declare class ClaudeSessionService {
182
183
  sessionNameSuggestion(sessionId: string, signal?: AbortSignal): Promise<string | null>;
183
184
  sessions(): Promise<SessionSummary[]>;
184
185
  inspect(sessionId: string): Promise<SessionInspection>;
186
+ readEffectiveAgentColor(sessionId: string): Promise<AgentColorName | undefined>;
185
187
  export(sessionId: string): Promise<Buffer>;
186
188
  transcript(sessionId: string, resumeSessionAt?: string): Promise<ClaudeDisplayTranscriptItem[]>;
187
189
  rename(sessionId: string, name: string): Promise<void>;
@@ -190,6 +192,9 @@ export declare class ClaudeSessionService {
190
192
  approveRecentlyDenied(sessionId: string, display: string): Promise<void>;
191
193
  retryRecentlyDenied(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
192
194
  recordBtwUsage(sessionId: string | undefined, permissionMode?: ClaudePermissionMode): Promise<string>;
195
+ recordColorUsage(sessionId: string | undefined, selection: AgentColorSelection, display: string, permissionMode?: ClaudePermissionMode, options?: {
196
+ createSession?: boolean;
197
+ }): Promise<string>;
193
198
  recordBackgroundUsage(sessionId: string | undefined, permissionMode?: ClaudePermissionMode): Promise<string>;
194
199
  recordBackgroundLaunch(sessionId: string): Promise<SessionForkCheckpoint>;
195
200
  compact(sessionId: string, signal?: AbortSignal, selection?: ManualCompactSelection): Promise<ManualCompactResult>;
@@ -214,6 +219,7 @@ export declare class ClaudeSessionService {
214
219
  private restoreWorktree;
215
220
  private paths;
216
221
  private appendCdCommand;
222
+ private appendAgentColorUsage;
217
223
  private appendSystemLocalCommand;
218
224
  private ensureLocalSession;
219
225
  private appendInputHistory;
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { appendFile, copyFile, link, lstat, mkdir, readdir, realpath, stat, unlink, } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, extname, isAbsolute, join, relative } from 'node:path';
5
+ import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
5
6
  import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
6
7
  import { isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
7
8
  import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
@@ -696,6 +697,11 @@ export class ClaudeSessionService {
696
697
  : {}),
697
698
  };
698
699
  }
700
+ async readEffectiveAgentColor(sessionId) {
701
+ this.assertSessionPersistence();
702
+ const recovery = await this.store(sessionId).loadReadOnly();
703
+ return getClaudeEffectiveAgentColor(recovery.entries, sessionId);
704
+ }
699
705
  async export(sessionId) {
700
706
  this.assertSessionPersistence();
701
707
  try {
@@ -934,6 +940,21 @@ export class ClaudeSessionService {
934
940
  await new Promise((resolve) => setTimeout(resolve, 25));
935
941
  }
936
942
  }
943
+ async recordColorUsage(sessionId, selection, display, permissionMode = 'default', options = {}) {
944
+ const output = agentColorMessage(selection);
945
+ const agentColor = selection.kind === 'color'
946
+ ? selection.color
947
+ : selection.kind === 'reset'
948
+ ? AGENT_COLOR_DEFAULT
949
+ : undefined;
950
+ const createLocalSession = sessionId === undefined || options.createSession === true;
951
+ const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode, createLocalSession ? agentColor : undefined, options.createSession === true);
952
+ if (this.options.sessionPersistence !== false) {
953
+ await this.appendInputHistory(display, activeSessionId);
954
+ }
955
+ await this.appendAgentColorUsage(activeSessionId, display, output, createLocalSession ? undefined : agentColor);
956
+ return activeSessionId;
957
+ }
937
958
  async recordBackgroundUsage(sessionId, permissionMode = 'default') {
938
959
  this.assertWritable();
939
960
  const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode);
@@ -2566,6 +2587,35 @@ export class ClaudeSessionService {
2566
2587
  throw new Error(`Claude local command conflict: ${result.reason}`);
2567
2588
  }
2568
2589
  }
2590
+ async appendAgentColorUsage(sessionId, display, output, agentColor) {
2591
+ const args = display.replace(/^\/color\s*/u, '').trim();
2592
+ while (true) {
2593
+ const result = await this.turnStore(sessionId).withLease(async (lease) => {
2594
+ const snapshot = await lease.load();
2595
+ if (snapshot.entries.length === 0) {
2596
+ throw new Error(`Claude session not found: ${sessionId}`);
2597
+ }
2598
+ const appended = await lease.appendMany(snapshot.tail, [
2599
+ ...(agentColor === undefined
2600
+ ? []
2601
+ : [
2602
+ {
2603
+ type: 'agent-color',
2604
+ agentColor,
2605
+ sessionId,
2606
+ },
2607
+ ]),
2608
+ ...this.localCommandEntries(sessionId, this.activeCwd(), this.logicalTailUuid(snapshot.tail), 'color', args, output),
2609
+ ]);
2610
+ if (appended.status === 'conflict') {
2611
+ throw new Error(`Claude color local command append conflict: ${appended.reason}`);
2612
+ }
2613
+ });
2614
+ if (result.status === 'completed')
2615
+ return;
2616
+ await new Promise((resolve) => setTimeout(resolve, 25));
2617
+ }
2618
+ }
2569
2619
  async appendSystemLocalCommand(sessionId, command, args, output) {
2570
2620
  while (true) {
2571
2621
  const result = await this.turnStore(sessionId).withLease(async (lease) => {
@@ -2583,13 +2633,22 @@ export class ClaudeSessionService {
2583
2633
  await new Promise((resolve) => setTimeout(resolve, 25));
2584
2634
  }
2585
2635
  }
2586
- async ensureLocalSession(sessionId, permissionMode) {
2587
- if (sessionId)
2636
+ async ensureLocalSession(sessionId, permissionMode, agentColor, createExplicit = false) {
2637
+ if (sessionId !== undefined && !createExplicit)
2588
2638
  return sessionId;
2589
2639
  this.assertWritable();
2590
- const createdSessionId = randomUUID();
2640
+ const createdSessionId = sessionId ?? randomUUID();
2591
2641
  const store = this.turnStore(createdSessionId);
2592
2642
  const created = await store.create([
2643
+ ...(agentColor === undefined
2644
+ ? []
2645
+ : [
2646
+ {
2647
+ type: 'agent-color',
2648
+ agentColor,
2649
+ sessionId: createdSessionId,
2650
+ },
2651
+ ]),
2593
2652
  { type: 'mode', mode: 'normal', sessionId: createdSessionId },
2594
2653
  {
2595
2654
  type: 'permission-mode',
@@ -8,6 +8,8 @@ import { type TuiMemoryFiles } from './tui/memory-files.js';
8
8
  import { type TuiDiffSnapshot } from './tui/git-diff.js';
9
9
  import { type TuiPermissionBehavior, type TuiPermissionRule } from './tui/permission-settings.js';
10
10
  import { type RecentlyDeniedStore } from './tui/recently-denied.js';
11
+ import { type AgentColorSelection } from '../compatibility/claude/agent-color.js';
12
+ import type { AgentColorName } from '../compatibility/claude/agent-color.js';
11
13
  import type { ClaudeResourceScope } from '../compatibility/claude/shared-resources.js';
12
14
  import type { TuiHookConfiguration } from './tui/hook-settings.js';
13
15
  import { type TuiSlashCommand } from './tui/slash-commands.js';
@@ -41,6 +43,8 @@ interface InteractiveSessionCommands {
41
43
  retryRecentlyDenied?(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
42
44
  answerSideQuestion?(sessionId: string | undefined, question: string, signal?: AbortSignal, onDelta?: (delta: string) => void, permissionMode?: ClaudePermissionMode): Promise<SideQuestionResult>;
43
45
  recordBtwUsage?(sessionId: string | undefined, permissionMode?: ClaudePermissionMode): Promise<string>;
46
+ recordColorUsage?(sessionId: string | undefined, selection: AgentColorSelection, display: string, permissionMode?: ClaudePermissionMode): Promise<string>;
47
+ agentColor?(sessionId: string): Promise<AgentColorName | undefined>;
44
48
  recordBackgroundUsage?(sessionId: string | undefined, permissionMode?: ClaudePermissionMode): Promise<string>;
45
49
  recordBackgroundLaunch?(sessionId: string): Promise<SessionForkCheckpoint>;
46
50
  forkSideQuestion?(sessionId: string, question: string, signal?: AbortSignal): Promise<SideQuestionForkResult>;
@@ -111,6 +115,7 @@ interface InteractiveAppProps {
111
115
  initialSessions: readonly SessionSummary[];
112
116
  initialPrompt?: string;
113
117
  initialHistory?: readonly TranscriptItem[];
118
+ initialSessionColor?: AgentColorName;
114
119
  signal?: AbortSignal;
115
120
  onCancel?: () => void;
116
121
  onTurnChange?: (turn: Promise<void> | null) => void;
@@ -179,7 +184,7 @@ interface InteractiveAppProps {
179
184
  releaseNotesLoader?: (configRoot: string) => Promise<string>;
180
185
  settingSources?: readonly ClaudeResourceScope[];
181
186
  }
182
- export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
187
+ export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
183
188
  export declare function runInteractive(options: {
184
189
  factory: InteractiveServiceFactory;
185
190
  initialPrompt?: string;
@@ -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);
@@ -4951,6 +4998,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
4951
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',
4952
4999
  });
4953
5000
  }
5001
+ else if (colorCommand) {
5002
+ changeSessionColor(prompt, parseAgentColorInput(colorCommand[1] ?? ''));
5003
+ }
4954
5004
  else if (btwCommand) {
4955
5005
  const sideQuestion = btwCommand[1]?.trim();
4956
5006
  if (sideQuestion)
@@ -5420,7 +5470,9 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
5420
5470
  label: 'Save to file',
5421
5471
  description: 'Save the conversation to a file in the current directory',
5422
5472
  },
5423
- ], 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
5424
5476
  ? (() => {
5425
5477
  const duration = formatTurnDuration(turnDuration);
5426
5478
  return duration === undefined
@@ -5534,11 +5586,16 @@ export async function runInteractive(options) {
5534
5586
  initialAgentPromptResolved = true;
5535
5587
  }
5536
5588
  let initialHistory = [];
5589
+ let initialSessionColor;
5537
5590
  try {
5538
5591
  initialHistory =
5539
5592
  resume?.sessionId === undefined || listing.transcript === undefined
5540
5593
  ? []
5541
5594
  : await listing.transcript(resume.sessionId);
5595
+ initialSessionColor =
5596
+ resume?.sessionId === undefined || listing.agentColor === undefined
5597
+ ? undefined
5598
+ : await listing.agentColor(resume.sessionId);
5542
5599
  }
5543
5600
  catch (error) {
5544
5601
  try {
@@ -5563,7 +5620,7 @@ export async function runInteractive(options) {
5563
5620
  let cleanup = null;
5564
5621
  let backgrounded;
5565
5622
  rendererChange = null;
5566
- 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
5567
5624
  ? {}
5568
5625
  : { settingSources: options.settingSources }), initialThemeSettings: initialThemeSettings, ...(initialThemeLoadError === undefined
5569
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[];
@@ -34,6 +34,12 @@ export const BUILTIN_TUI_SLASH_COMMANDS = [
34
34
  description: 'Start a new session with empty context; previous session stays on disk (resumable with /resume)',
35
35
  source: 'builtin',
36
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
+ },
37
43
  {
38
44
  name: 'compact',
39
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>;
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { ClaudeSessionService, } from './application/session-service.js';
10
+ import { agentColorMessage, parseAgentColorInput, } from './compatibility/claude/agent-color.js';
10
11
  import { ClaudeConditionalRuleResolver, ClaudeContextAssembler, } from './compatibility/claude/context.js';
11
12
  import { loadClaudeDynamicContext } from './compatibility/claude/dynamic-context.js';
12
13
  import { createClaudePrSessionFilter, filterClaudePrLinkedSessions, } from './compatibility/claude/pr-links.js';
@@ -46,7 +47,7 @@ import { WorkspaceContext } from './application/session-worktree.js';
46
47
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
47
48
  import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
48
49
  import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
49
- import { createErrorResult, createSuccessResult, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
50
+ import { createErrorResult, createSuccessResult, matchHeadlessColorCommand, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
50
51
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
51
52
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
52
53
  import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
@@ -1343,6 +1344,8 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1343
1344
  retryRecentlyDenied: (sessionId, display, retrySignal) => service.retryRecentlyDenied(sessionId, display, retrySignal),
1344
1345
  answerSideQuestion: (sessionId, question, sideSignal, onDelta, permissionMode) => service.answerSideQuestion(sessionId, question, sideSignal, onDelta, permissionMode),
1345
1346
  recordBtwUsage: (sessionId, permissionMode) => service.recordBtwUsage(sessionId, permissionMode),
1347
+ recordColorUsage: (sessionId, selection, display, permissionMode, options) => service.recordColorUsage(sessionId, selection, display, permissionMode, options),
1348
+ agentColor: (sessionId) => service.readEffectiveAgentColor(sessionId),
1346
1349
  recordBackgroundUsage: (sessionId, permissionMode) => service.recordBackgroundUsage(sessionId, permissionMode),
1347
1350
  recordBackgroundLaunch: (sessionId) => service.recordBackgroundLaunch(sessionId),
1348
1351
  forkSideQuestion: (sessionId, question, sideSignal) => service.forkSideQuestion(sessionId, question, sideSignal),
@@ -3735,6 +3738,7 @@ async function execute(argv, io, dependencies, signal) {
3735
3738
  const pendingEvents = [];
3736
3739
  let streamIterator;
3737
3740
  let firstStreamMessage;
3741
+ let streamInputExhausted = false;
3738
3742
  const queuedStreamUsers = [];
3739
3743
  const earlyControlResponses = new Map();
3740
3744
  let currentTurnAbort;
@@ -3934,6 +3938,19 @@ async function execute(argv, io, dependencies, signal) {
3934
3938
  }
3935
3939
  return 0;
3936
3940
  }
3941
+ const headlessTurnReached = invocation.rewindFiles === undefined &&
3942
+ !['sessions', 'fork', 'inspect', 'export'].includes(command ?? 'run');
3943
+ if (streamIterator && headlessTurnReached) {
3944
+ const first = await nextStreamUser();
3945
+ if (first)
3946
+ firstStreamMessage = first;
3947
+ else
3948
+ streamInputExhausted = true;
3949
+ }
3950
+ const firstHeadlessPrompt = firstStreamMessage?.prompt ?? headlessPrompt;
3951
+ const firstTurnIsLocalColor = !invocation.disableSlashCommands &&
3952
+ firstHeadlessPrompt !== undefined &&
3953
+ matchHeadlessColorCommand(firstHeadlessPrompt) !== undefined;
3937
3954
  const service = await dependencies.createService({
3938
3955
  eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
3939
3956
  ? (event) => {
@@ -3958,8 +3975,7 @@ async function execute(argv, io, dependencies, signal) {
3958
3975
  }
3959
3976
  }
3960
3977
  : eventSink(io, outputFormat, invocation.legacyJson),
3961
- requireProvider: invocation.rewindFiles === undefined &&
3962
- !['fork', 'sessions', 'inspect', 'export'].includes(command ?? 'run'),
3978
+ requireProvider: !streamInputExhausted && !firstTurnIsLocalColor && headlessTurnReached,
3963
3979
  ...(retryInterruptedTools ? { approveRecovery: () => true } : {}),
3964
3980
  ...(streamIterator ? { approveTool: approveStreamTool } : {}),
3965
3981
  ...(streamIterator ? { onElicitation: respondStreamElicitation } : {}),
@@ -4067,12 +4083,8 @@ async function execute(argv, io, dependencies, signal) {
4067
4083
  if (outputFormat === 'stream-json' && !invocation.legacyJson) {
4068
4084
  streamOutput = new StreamJsonOutput((value) => writeJson(io, value), runtimeInfo, activeSessionId, includePartialMessages, invocation.includeHookEvents);
4069
4085
  }
4070
- if (streamIterator) {
4071
- const first = await nextStreamUser();
4072
- if (!first)
4073
- return 0;
4074
- firstStreamMessage = first;
4075
- }
4086
+ if (streamInputExhausted)
4087
+ return 0;
4076
4088
  const initialPrompt = firstStreamMessage?.prompt ??
4077
4089
  promptFrom(command === 'resume'
4078
4090
  ? args.slice(2)
@@ -4099,15 +4111,38 @@ async function execute(argv, io, dependencies, signal) {
4099
4111
  streamOutput.replayUser(streamMessage.message);
4100
4112
  }
4101
4113
  let result;
4114
+ let colorArgs;
4102
4115
  try {
4103
- result =
4104
- existingSessionId !== undefined || !isFirstTurn
4105
- ? runSignal
4106
- ? await service.resume(activeSessionId, prompt, runSignal, undefined, streamMessage?.images, streamMessage?.documents)
4107
- : await service.resume(activeSessionId, prompt, undefined, undefined, streamMessage?.images, streamMessage?.documents)
4108
- : runSignal
4109
- ? await service.run(prompt, runSignal, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents)
4110
- : await service.run(prompt, undefined, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents);
4116
+ colorArgs = invocation.disableSlashCommands
4117
+ ? undefined
4118
+ : matchHeadlessColorCommand(prompt);
4119
+ if (colorArgs !== undefined) {
4120
+ if (!service.recordColorUsage) {
4121
+ throw new Error('Session color is unavailable.');
4122
+ }
4123
+ const selection = parseAgentColorInput(colorArgs);
4124
+ const localSessionId = await service.recordColorUsage(activeSessionId, selection, prompt, invocation.permissionMode, isFirstTurn && existingSessionId === undefined
4125
+ ? { createSession: true }
4126
+ : undefined);
4127
+ result = {
4128
+ sessionId: localSessionId,
4129
+ text: agentColorMessage(selection),
4130
+ usage: { inputTokens: 0, outputTokens: 0 },
4131
+ durationApiMs: 0,
4132
+ costUsd: 0,
4133
+ modelUsage: {},
4134
+ };
4135
+ }
4136
+ else {
4137
+ result =
4138
+ existingSessionId !== undefined || !isFirstTurn
4139
+ ? runSignal
4140
+ ? await service.resume(activeSessionId, prompt, runSignal, undefined, streamMessage?.images, streamMessage?.documents)
4141
+ : await service.resume(activeSessionId, prompt, undefined, undefined, streamMessage?.images, streamMessage?.documents)
4142
+ : runSignal
4143
+ ? await service.run(prompt, runSignal, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents)
4144
+ : await service.run(prompt, undefined, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents);
4145
+ }
4111
4146
  }
4112
4147
  catch (error) {
4113
4148
  if (isCancellation(error, turnAbort.signal)) {
@@ -4134,16 +4169,20 @@ async function execute(argv, io, dependencies, signal) {
4134
4169
  return false;
4135
4170
  }
4136
4171
  activeSessionId = result.sessionId;
4137
- if (streamOutput)
4172
+ if (streamOutput) {
4173
+ if (colorArgs !== undefined) {
4174
+ streamOutput.syntheticAssistant(result.text);
4175
+ }
4138
4176
  streamOutput.result(result, startedAt);
4177
+ }
4139
4178
  else if (outputFormat === 'json') {
4140
4179
  const resultRuntimeInfo = service.runtimeInfo?.() ?? runtimeInfo;
4141
- writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, Math.max(1, jsonModelTurns)));
4180
+ writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, colorArgs !== undefined ? 0 : Math.max(1, jsonModelTurns)));
4142
4181
  }
4143
4182
  else if (outputFormat !== 'text')
4144
4183
  writeJson(io, { type: 'result', ...result });
4145
4184
  else
4146
- io.stdout('\n');
4185
+ io.stdout(colorArgs !== undefined ? `${result.text}\n` : '\n');
4147
4186
  if (streamOutput && invocation.promptSuggestions) {
4148
4187
  try {
4149
4188
  const suggestion = await service.promptSuggestion?.(activeSessionId, runSignal);
@@ -0,0 +1,25 @@
1
+ import type { ClaudeTranscriptEntry } from './schema.js';
2
+ export declare const AGENT_COLORS: readonly ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"];
3
+ export type AgentColorName = (typeof AGENT_COLORS)[number];
4
+ export declare const AGENT_COLOR_DEFAULT: "default";
5
+ export type AgentColorValue = AgentColorName | typeof AGENT_COLOR_DEFAULT;
6
+ export declare const AGENT_COLOR_VALUES: readonly AgentColorValue[];
7
+ export declare const AGENT_COLOR_CHOICES: string;
8
+ export declare const RESET_AGENT_COLOR_ALIASES: readonly ["default", "reset", "none", "gray", "grey"];
9
+ export declare function isAgentColorName(value: unknown): value is AgentColorName;
10
+ export declare function isAgentColorValue(value: unknown): value is AgentColorValue;
11
+ export declare function normalizeAgentColorInput(input: string): string;
12
+ export type AgentColorSelection = {
13
+ kind: 'color';
14
+ color: AgentColorName;
15
+ } | {
16
+ kind: 'reset';
17
+ } | {
18
+ kind: 'invalid';
19
+ input: string;
20
+ };
21
+ export declare function parseAgentColorInput(input: string): AgentColorSelection;
22
+ export declare function randomAgentColor(): AgentColorName;
23
+ export declare function agentColorMessage(selection: AgentColorSelection): string;
24
+ export declare function getClaudeEffectiveAgentColor(entries: readonly ClaudeTranscriptEntry[], sessionId: string): AgentColorName | undefined;
25
+ //# sourceMappingURL=agent-color.d.ts.map
@@ -0,0 +1,79 @@
1
+ export const AGENT_COLORS = [
2
+ 'red',
3
+ 'blue',
4
+ 'green',
5
+ 'yellow',
6
+ 'purple',
7
+ 'orange',
8
+ 'pink',
9
+ 'cyan',
10
+ ];
11
+ export const AGENT_COLOR_DEFAULT = 'default';
12
+ export const AGENT_COLOR_VALUES = [
13
+ ...AGENT_COLORS,
14
+ AGENT_COLOR_DEFAULT,
15
+ ];
16
+ export const AGENT_COLOR_CHOICES = `${AGENT_COLORS.join(', ')}, ${AGENT_COLOR_DEFAULT}`;
17
+ export const RESET_AGENT_COLOR_ALIASES = [
18
+ AGENT_COLOR_DEFAULT,
19
+ 'reset',
20
+ 'none',
21
+ 'gray',
22
+ 'grey',
23
+ ];
24
+ export function isAgentColorName(value) {
25
+ return (typeof value === 'string' &&
26
+ AGENT_COLORS.includes(value));
27
+ }
28
+ export function isAgentColorValue(value) {
29
+ return (typeof value === 'string' &&
30
+ AGENT_COLOR_VALUES.includes(value));
31
+ }
32
+ export function normalizeAgentColorInput(input) {
33
+ return input.trim().toLowerCase();
34
+ }
35
+ export function parseAgentColorInput(input) {
36
+ const normalized = normalizeAgentColorInput(input);
37
+ if (normalized === '') {
38
+ return { kind: 'color', color: randomAgentColor() };
39
+ }
40
+ if (RESET_AGENT_COLOR_ALIASES.includes(normalized)) {
41
+ return { kind: 'reset' };
42
+ }
43
+ if (isAgentColorName(normalized)) {
44
+ return { kind: 'color', color: normalized };
45
+ }
46
+ return { kind: 'invalid', input: normalized };
47
+ }
48
+ export function randomAgentColor() {
49
+ const index = Math.floor(Math.random() * AGENT_COLORS.length);
50
+ const color = AGENT_COLORS[index];
51
+ if (color === undefined)
52
+ throw new Error('AGENT_COLORS must not be empty');
53
+ return color;
54
+ }
55
+ export function agentColorMessage(selection) {
56
+ switch (selection.kind) {
57
+ case 'color':
58
+ return `Session color set to: ${selection.color}`;
59
+ case 'reset':
60
+ return 'Session color reset to default';
61
+ case 'invalid':
62
+ return `Invalid color "${selection.input}". Available colors: ${AGENT_COLOR_CHOICES}`;
63
+ }
64
+ }
65
+ export function getClaudeEffectiveAgentColor(entries, sessionId) {
66
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
67
+ const entry = entries[index];
68
+ if (entry?.type !== 'agent-color' || entry.sessionId !== sessionId) {
69
+ continue;
70
+ }
71
+ const color = entry.agentColor;
72
+ if (color === AGENT_COLOR_DEFAULT)
73
+ return undefined;
74
+ if (isAgentColorName(color))
75
+ return color;
76
+ }
77
+ return undefined;
78
+ }
79
+ //# sourceMappingURL=agent-color.js.map
@@ -152,6 +152,7 @@ function validateNativeHistory(entries) {
152
152
  return undefined;
153
153
  }
154
154
  export function createClaudeNativeFork({ source, sourceSessionId, sessionId, resumeSessionAt, }) {
155
+ const agentColors = [];
155
156
  const titles = [];
156
157
  const modes = [];
157
158
  const permissionModes = [];
@@ -204,7 +205,9 @@ export function createClaudeNativeFork({ source, sourceSessionId, sessionId, res
204
205
  throw new Error('Claude fork source entry has the wrong sessionId');
205
206
  }
206
207
  const copied = copyClaudeEntryWithSessionId(entry, sessionId);
207
- if (entry.type === 'ai-title')
208
+ if (entry.type === 'agent-color')
209
+ agentColors.push(copied);
210
+ else if (entry.type === 'ai-title')
208
211
  titles.push(copied);
209
212
  else if (entry.type === 'mode')
210
213
  modes.push(copied);
@@ -232,6 +235,7 @@ export function createClaudeNativeFork({ source, sourceSessionId, sessionId, res
232
235
  lastPrompt = undefined;
233
236
  }
234
237
  return [
238
+ ...agentColors.slice(-1),
235
239
  ...titles.slice(-1),
236
240
  ...modes.slice(-1),
237
241
  ...permissionModes.slice(-1),
@@ -1,5 +1,7 @@
1
+ import { isAgentColorValue } from './agent-color.js';
1
2
  const SUPPORTED_VERSION = '2.1.208';
2
3
  const APPENDABLE_ENTRY_TYPES = new Set([
4
+ 'agent-color',
3
5
  'agent-name',
4
6
  'agent-setting',
5
7
  'assistant',
@@ -17,6 +19,7 @@ const APPENDABLE_ENTRY_TYPES = new Set([
17
19
  'worktree-state',
18
20
  ]);
19
21
  const FORKABLE_ENTRY_TYPES = new Set([
22
+ 'agent-color',
20
23
  'agent-name',
21
24
  'agent-setting',
22
25
  'ai-title',
@@ -717,6 +720,13 @@ function validateAppendableEntry(entry) {
717
720
  }
718
721
  return;
719
722
  }
723
+ if (entry.type === 'agent-color') {
724
+ if (!isAgentColorValue(entry.agentColor) ||
725
+ !isNonEmptyString(entry.sessionId)) {
726
+ throw new Error('Claude agent-color entry has invalid metadata');
727
+ }
728
+ return;
729
+ }
720
730
  if (entry.type === 'agent-name') {
721
731
  if (!isNonEmptyString(entry.agentName) ||
722
732
  !isNonEmptyString(entry.sessionId)) {
@@ -868,7 +878,9 @@ function validateSidechainEntry(entry) {
868
878
  }
869
879
  }
870
880
  function validateForkableEntry(entry) {
871
- if (entry.type === 'custom-title' || entry.type === 'agent-name') {
881
+ if (entry.type === 'custom-title' ||
882
+ entry.type === 'agent-name' ||
883
+ entry.type === 'agent-color') {
872
884
  validateAppendableEntry(entry);
873
885
  return;
874
886
  }
@@ -16,6 +16,7 @@ export class ClaudeTranscriptParseError extends Error {
16
16
  }
17
17
  const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
18
18
  const NON_TAIL_ENTRY_TYPES = new Set([
19
+ 'agent-color',
19
20
  'agent-name',
20
21
  'agent-setting',
21
22
  'custom-title',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -93,6 +93,7 @@
93
93
  "test:subagent-compat": "npm run build && node scripts/verify-subagent-compatibility.mjs",
94
94
  "test:background-agent-compat": "npm run build && node scripts/verify-background-agent-compatibility.mjs",
95
95
  "test:background-command-compat": "npm run build && node scripts/verify-background-command-compatibility.mjs",
96
+ "test:color-command-compat": "npm run build && node scripts/verify-color-command-compatibility.mjs",
96
97
  "test:task-compat": "npm run build && node scripts/verify-task-compatibility.mjs",
97
98
  "test:scheduled-compat": "npm run build && node scripts/verify-scheduled-tools-compatibility.mjs",
98
99
  "test:top-level-agent-compat": "npm run build && node scripts/verify-top-level-agent-compatibility.mjs",