praxis-agent 0.46.0 → 0.46.2

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
@@ -212,9 +212,13 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
212
212
  installing it, rejects concurrent updates, and can roll back after an
213
213
  interruption or crash.
214
214
 
215
- Detailed feature status and executable evidence live in the
216
- [parity matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md),
217
- not in this entry-point README.
215
+ Current qualification status and executable evidence live in the
216
+ [Native Fixture Contracts](https://github.com/Forest-Isle/Praxis/blob/main/docs/NATIVE_FIXTURE_CONTRACTS.md)
217
+ and its machine-readable
218
+ [fixture manifest](https://github.com/Forest-Isle/Praxis/blob/main/test/fixtures/manifest.json).
219
+ The [parity matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md)
220
+ and [roadmap](https://github.com/Forest-Isle/Praxis/blob/main/docs/ROADMAP.md) are
221
+ historical clean-room records.
218
222
 
219
223
  ## Native data plane
220
224
 
@@ -234,17 +238,17 @@ only; they do not change Praxis data ownership.
234
238
 
235
239
  ## Documentation
236
240
 
237
- | Need | Document |
238
- | ------------------------------------------ | ------------------------------------------------------------------------------------------------ |
239
- | Install and run the first session | [Getting Started](https://github.com/Forest-Isle/Praxis/blob/main/docs/GETTING_STARTED.md) |
240
- | Common commands and environment variables | [CLI Reference](https://github.com/Forest-Isle/Praxis/blob/main/docs/CLI_REFERENCE.md) |
241
- | Find all user and maintainer documentation | [Documentation Index](https://github.com/Forest-Isle/Praxis/blob/main/docs/README.md) |
242
- | Understand module and data-flow boundaries | [Architecture](https://github.com/Forest-Isle/Praxis/blob/main/docs/ARCHITECTURE.md) |
243
- | Review security assumptions | [Threat Model](https://github.com/Forest-Isle/Praxis/blob/main/docs/THREAT_MODEL.md) |
244
- | Check Claude Code parity | [Parity Matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md) |
245
- | Review interactive TUI design and evidence | [Quiet Operator Spec](https://github.com/Forest-Isle/Praxis/blob/main/docs/TUI_REDESIGN_SPEC.md) |
246
- | Build, test, and contribute | [Contributing](https://github.com/Forest-Isle/Praxis/blob/main/CONTRIBUTING.md) |
247
- | Verify release and supply-chain controls | [Release Contract](https://github.com/Forest-Isle/Praxis/blob/main/docs/RELEASE.md) |
241
+ | Need | Document |
242
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
243
+ | Install and run the first session | [Getting Started](https://github.com/Forest-Isle/Praxis/blob/main/docs/GETTING_STARTED.md) |
244
+ | Common commands and environment variables | [CLI Reference](https://github.com/Forest-Isle/Praxis/blob/main/docs/CLI_REFERENCE.md) |
245
+ | Find all user and maintainer documentation | [Documentation Index](https://github.com/Forest-Isle/Praxis/blob/main/docs/README.md) |
246
+ | Understand module and data-flow boundaries | [Architecture](https://github.com/Forest-Isle/Praxis/blob/main/docs/ARCHITECTURE.md) |
247
+ | Review security assumptions | [Threat Model](https://github.com/Forest-Isle/Praxis/blob/main/docs/THREAT_MODEL.md) |
248
+ | Qualify native behavior and evidence | [Native Fixture Contracts](https://github.com/Forest-Isle/Praxis/blob/main/docs/NATIVE_FIXTURE_CONTRACTS.md) |
249
+ | Review interactive TUI design and evidence | [Quiet Operator Spec](https://github.com/Forest-Isle/Praxis/blob/main/docs/TUI_REDESIGN_SPEC.md) |
250
+ | Build, test, and contribute | [Contributing](https://github.com/Forest-Isle/Praxis/blob/main/CONTRIBUTING.md) |
251
+ | Verify release and supply-chain controls | [Release Contract](https://github.com/Forest-Isle/Praxis/blob/main/docs/RELEASE.md) |
248
252
 
249
253
  ## Project boundary
250
254
 
@@ -286,10 +290,13 @@ injected regression protection, plus Quiet Operator input echo `<50 ms` and
286
290
  normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
287
291
  `npm run check` also enforces the corresponding source dependency direction.
288
292
  `npm run test:coverage` measures all production code under `src/**` with V8 and
289
- enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines.
290
- `npm run test:core-completion` runs the 56-story #402 audit and reports
291
- implemented, qualified, blocked, deferred, and out-of-scope states separately;
292
- it never treats missing live prerequisites as a pass.
293
+ enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
294
+ and rejects any production runtime module with zero covered statements (while allowing
295
+ type-only modules). `npm run test:fixtures` executes the 67-behavior native contract; 59 behaviors
296
+ are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
297
+ performs the structural check and is part of `npm run check`.
298
+ `npm run test:core-completion` is retained as a compatibility alias for
299
+ `npm run test:fixtures`.
293
300
 
294
301
  Contributions use Conventional Commit pull-request titles and the protected
295
302
  squash-merge workflow. Read
@@ -123,6 +123,11 @@ export interface SessionRunResult {
123
123
  costUsd?: number;
124
124
  modelUsage?: Readonly<Record<string, ModelUsage>>;
125
125
  }
126
+ export interface CwdInspection {
127
+ readonly canonicalTarget: string;
128
+ readonly canonicalCurrentCwd: string;
129
+ readonly sameDirectory: boolean;
130
+ }
126
131
  export interface SideQuestionResult {
127
132
  sessionId: string;
128
133
  text: string;
@@ -290,7 +295,8 @@ export declare class ClaudeSessionService {
290
295
  metadata(sessionId: string): Promise<ClaudeSessionMetadata>;
291
296
  rename(sessionId: string, name: string): Promise<void>;
292
297
  tag(sessionId: string, tag: string): Promise<void>;
293
- changeCwd(sessionId: string | undefined, requestedCwd: string): Promise<string>;
298
+ changeCwd(sessionId: string | undefined, requestedCwd: string, expectedCanonicalTarget?: string): Promise<string>;
299
+ inspectCwd(requestedCwd: string): Promise<CwdInspection>;
294
300
  recordCdUsage(sessionId: string): Promise<void>;
295
301
  approveRecentlyDenied(sessionId: string, display: string): Promise<void>;
296
302
  retryRecentlyDenied(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
@@ -1806,17 +1806,23 @@ export class ClaudeSessionService {
1806
1806
  const normalized = tag.trim();
1807
1807
  await this.appendNativeCommand(sessionId, `/tag ${normalized}`);
1808
1808
  }
1809
- async changeCwd(sessionId, requestedCwd) {
1809
+ async changeCwd(sessionId, requestedCwd, expectedCanonicalTarget) {
1810
1810
  this.assertWritable();
1811
1811
  const previousCwd = this.activeCwd();
1812
- const expandedCwd = requestedCwd === '~'
1813
- ? homedir()
1814
- : requestedCwd.startsWith('~/')
1815
- ? join(homedir(), requestedCwd.slice(2))
1816
- : requestedCwd;
1817
- const cwd = await realpath(isAbsolute(expandedCwd) ? expandedCwd : join(previousCwd, expandedCwd));
1818
- if (!(await stat(cwd)).isDirectory()) {
1819
- throw new Error(`Not a directory: ${requestedCwd}`);
1812
+ let inspection;
1813
+ try {
1814
+ inspection = await this.inspectCwd(requestedCwd);
1815
+ }
1816
+ catch (error) {
1817
+ if (error instanceof Error &&
1818
+ error.message.includes(' is not a directory.'))
1819
+ throw new Error(`Not a directory: ${requestedCwd}; ${error.message}`);
1820
+ throw error;
1821
+ }
1822
+ const cwd = inspection.canonicalTarget;
1823
+ if (expectedCanonicalTarget !== undefined &&
1824
+ cwd !== expectedCanonicalTarget) {
1825
+ throw new Error(`Directory changed after approval: expected ${expectedCanonicalTarget}, resolved to ${cwd}. Review the target and try again.`);
1820
1826
  }
1821
1827
  if (sessionId && this.options.sessionPersistence !== false) {
1822
1828
  const sourcePaths = this.pathsForCwd(sessionId, this.sessionCwds.get(sessionId) ?? previousCwd);
@@ -1896,6 +1902,33 @@ export class ClaudeSessionService {
1896
1902
  }
1897
1903
  return cwd;
1898
1904
  }
1905
+ async inspectCwd(requestedCwd) {
1906
+ const current = await realpath(this.activeCwd());
1907
+ const expanded = requestedCwd === '~'
1908
+ ? homedir()
1909
+ : requestedCwd.startsWith('~/')
1910
+ ? join(homedir(), requestedCwd.slice(2))
1911
+ : requestedCwd;
1912
+ const resolved = isAbsolute(expanded) ? expanded : join(current, expanded);
1913
+ let target;
1914
+ try {
1915
+ target = await realpath(resolved);
1916
+ }
1917
+ catch (error) {
1918
+ if (error.code === 'ENOENT')
1919
+ throw new Error(`Could not find a directory at ${resolved}.`);
1920
+ throw error;
1921
+ }
1922
+ const info = await stat(target);
1923
+ if (!info.isDirectory()) {
1924
+ throw new Error(`${target} is not a directory. Did you mean ${dirname(target)}?`);
1925
+ }
1926
+ return {
1927
+ canonicalTarget: target,
1928
+ canonicalCurrentCwd: current,
1929
+ sameDirectory: target === current,
1930
+ };
1931
+ }
1899
1932
  async recordCdUsage(sessionId) {
1900
1933
  this.assertWritable();
1901
1934
  const transcript = new NativeSessionTranscript({
@@ -1,4 +1,4 @@
1
- import type { ForkResult, ManualCompactResult, ManualCompactSelection, RewindPoint, SessionForkCheckpoint, SessionRunResult, SessionSummary, SideQuestionForkResult, SideQuestionResult } from '../application/session-service.js';
1
+ import type { CwdInspection, ForkResult, ManualCompactResult, ManualCompactSelection, RewindPoint, SessionForkCheckpoint, SessionRunResult, SessionSummary, SideQuestionForkResult, SideQuestionResult } from '../application/session-service.js';
2
2
  import type { ClaudeSessionCostSnapshot } from '../application/session-cost-tracker.js';
3
3
  import type { ModelImage, ModelToolCall, PermissionApproval, PermissionDecision, RuntimeEventSink } from '../core/runtime.js';
4
4
  import type { ActiveTurnInputCommandResult } from '../core/active-turn-input.js';
@@ -46,7 +46,8 @@ interface InteractiveSessionCommands {
46
46
  compact?(sessionId: string, signal?: AbortSignal, selection?: ManualCompactSelection): Promise<ManualCompactResult>;
47
47
  rewindFiles?(sessionId: string, userMessageId: string): Promise<void>;
48
48
  rewindPoints?(sessionId: string): Promise<RewindPoint[]>;
49
- changeCwd?(sessionId: string | undefined, cwd: string): Promise<string>;
49
+ changeCwd?(sessionId: string | undefined, cwd: string, expectedCanonicalTarget?: string): Promise<string>;
50
+ inspectCwd?(cwd: string): Promise<CwdInspection>;
50
51
  notify?(sessionId: string | undefined, message: string, notificationType: string, title?: string): void;
51
52
  recordCdUsage?(sessionId: string): Promise<void>;
52
53
  approveRecentlyDenied?(sessionId: string, display: string): Promise<void>;
@@ -44,6 +44,7 @@ import { projectTuiCommandPalette, tuiCommandPaletteCommandId, } from './tui/com
44
44
  import { runDoctor, } from '../maintenance/doctor.js';
45
45
  import { canonicalClaudeCostModelName, formatCostSummary, } from './tui/cost-summary.js';
46
46
  import { createComposerEditor, deleteComposerBackward, deleteComposerForward, insertComposerText, moveComposerCursor, } from './tui/composer-editor.js';
47
+ import { createComposerPromptHistory, navigateComposerPromptHistory, recordComposerPrompt, resetComposerPromptHistoryNavigation, seedComposerPromptHistory, } from './tui/composer-prompt-history.js';
47
48
  import { routeComposerKey, } from './tui/composer-key-router.js';
48
49
  import { routeTuiInteraction, } from './tui/tui-interaction-router.js';
49
50
  import { currentTuiInteractionLayer, projectTuiFocusStack, } from './tui/tui-focus-stack.js';
@@ -239,6 +240,9 @@ const HIDDEN_TUI_SLASH_COMMANDS = new Set([
239
240
  'update',
240
241
  'usage',
241
242
  ]);
243
+ function transcriptPrompts(items) {
244
+ return items.flatMap((item) => (item.kind === 'user' ? [item.text] : []));
245
+ }
242
246
  /** Advances the React-owned transcript plus its exact local mutation fact. */
243
247
  export function advanceInteractiveHistoryState(current, items, changedFrom) {
244
248
  if (!Number.isInteger(changedFrom) ||
@@ -372,9 +376,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
372
376
  const [pendingFork, setPendingFork] = useState(resume?.forkSession === true);
373
377
  const inputRef = useRef('');
374
378
  const inputCursorRef = useRef(0);
375
- const inputHistoryRef = useRef([]);
376
- const inputHistoryIndexRef = useRef(null);
377
- const inputHistoryDraftRef = useRef('');
379
+ const promptHistoryRef = useRef(seedComposerPromptHistory(transcriptPrompts(initialHistory)));
378
380
  const [pendingInputs, setPendingInputs] = useState([]);
379
381
  const pendingInputsRef = useRef(pendingInputs);
380
382
  pendingInputsRef.current = pendingInputs;
@@ -608,6 +610,35 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
608
610
  const planApprovalRef = useRef(null);
609
611
  const [planApprovalSelection, setPlanApprovalSelection] = useState(0);
610
612
  const [planApprovalFeedbackMode, setPlanApprovalFeedbackMode] = useState(false);
613
+ const [cdTrust, setCdTrust] = useState(null);
614
+ const [cdTrustSelection, setCdTrustSelection] = useState(0);
615
+ const cdTrustRef = useRef(null);
616
+ const trustedCwdsBySessionRef = useRef(new Map());
617
+ const trustedCwdSessionRef = useRef(sessionId);
618
+ useEffect(() => {
619
+ const previousSessionId = trustedCwdSessionRef.current;
620
+ if (previousSessionId === sessionId)
621
+ return;
622
+ if (previousSessionId === null && sessionId !== null) {
623
+ const pendingSessionTrust = trustedCwdsBySessionRef.current.get(null);
624
+ if (pendingSessionTrust)
625
+ trustedCwdsBySessionRef.current.set(sessionId, pendingSessionTrust);
626
+ trustedCwdsBySessionRef.current.delete(null);
627
+ }
628
+ else if (sessionId === null) {
629
+ trustedCwdsBySessionRef.current.delete(null);
630
+ }
631
+ trustedCwdSessionRef.current = sessionId;
632
+ }, [sessionId]);
633
+ const trustedCwds = () => {
634
+ const key = sessionIdRef.current;
635
+ const existing = trustedCwdsBySessionRef.current.get(key);
636
+ if (existing)
637
+ return existing;
638
+ const created = new Set();
639
+ trustedCwdsBySessionRef.current.set(key, created);
640
+ return created;
641
+ };
611
642
  const serviceRef = useRef(null);
612
643
  const serviceCreationRef = useRef(undefined);
613
644
  const onCleanupRef = useRef(onCleanup);
@@ -866,6 +897,13 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
866
897
  answer: input,
867
898
  })
868
899
  : null, [question, input]);
900
+ const cdTrustSurface = useMemo(() => cdTrust
901
+ ? projectTuiDecisionSurface({
902
+ kind: 'cd-trust',
903
+ canonicalPath: cdTrust.canonicalPath,
904
+ selectedIndex: cdTrustSelection,
905
+ })
906
+ : null, [cdTrust, cdTrustSelection]);
869
907
  const elicitationSurface = useMemo(() => elicitation === null
870
908
  ? null
871
909
  : projectTuiElicitationSurface({
@@ -1127,16 +1165,18 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1127
1165
  }
1128
1166
  : planApproval !== null && planDecisionSurface !== null
1129
1167
  ? { priority: planDecisionSurface }
1130
- : question !== null && questionDecisionSurface !== null
1131
- ? { priority: questionDecisionSurface }
1132
- : elicitation !== null && elicitationSurface !== null
1133
- ? {
1134
- priority: {
1135
- kind: 'elicitation',
1136
- surface: elicitationSurface,
1137
- },
1138
- }
1139
- : {}),
1168
+ : cdTrust !== null && cdTrustSurface !== null
1169
+ ? { priority: cdTrustSurface }
1170
+ : question !== null && questionDecisionSurface !== null
1171
+ ? { priority: questionDecisionSurface }
1172
+ : elicitation !== null && elicitationSurface !== null
1173
+ ? {
1174
+ priority: {
1175
+ kind: 'elicitation',
1176
+ surface: elicitationSurface,
1177
+ },
1178
+ }
1179
+ : {}),
1140
1180
  ...(secondarySurface === undefined
1141
1181
  ? {}
1142
1182
  : { secondary: secondarySurface }),
@@ -1154,6 +1194,8 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1154
1194
  permission,
1155
1195
  permissionPrioritySurface,
1156
1196
  planApproval,
1197
+ cdTrust,
1198
+ cdTrustSurface,
1157
1199
  question,
1158
1200
  planDecisionSurface,
1159
1201
  questionDecisionSurface,
@@ -1466,6 +1508,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1466
1508
  fileSelectionRef.current = 0;
1467
1509
  };
1468
1510
  const updateComposerEditor = (editor, recordUndo = true) => {
1511
+ promptHistoryRef.current = resetComposerPromptHistoryNavigation(promptHistoryRef.current);
1469
1512
  if (recordUndo && editor.text !== inputRef.current) {
1470
1513
  undoStackRef.current = [
1471
1514
  ...undoStackRef.current,
@@ -1474,7 +1517,10 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1474
1517
  }
1475
1518
  updateComposerInput(editor.text, editor.cursor);
1476
1519
  };
1477
- const clearComposerInput = () => updateComposerInput('');
1520
+ const clearComposerInput = () => {
1521
+ promptHistoryRef.current = resetComposerPromptHistoryNavigation(promptHistoryRef.current);
1522
+ updateComposerInput('');
1523
+ };
1478
1524
  const promptImages = (prompt) => {
1479
1525
  const seen = new Set();
1480
1526
  return composerImageIds(prompt).flatMap((id) => {
@@ -1706,36 +1752,13 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1706
1752
  return () => clearInterval(timer);
1707
1753
  }, [menu?.kind]);
1708
1754
  const appendPromptHistory = (prompt) => {
1709
- if (!prompt)
1710
- return;
1711
- const history = inputHistoryRef.current.filter((item) => item !== prompt);
1712
- inputHistoryRef.current = [prompt, ...history].slice(0, 100);
1713
- inputHistoryIndexRef.current = null;
1714
- inputHistoryDraftRef.current = '';
1755
+ promptHistoryRef.current = recordComposerPrompt(promptHistoryRef.current, prompt);
1715
1756
  };
1716
1757
  const restorePromptHistory = (direction) => {
1717
- const history = inputHistoryRef.current;
1718
- if (history.length === 0)
1719
- return;
1720
- const currentIndex = inputHistoryIndexRef.current;
1721
- if (direction === 'previous') {
1722
- if (currentIndex === null)
1723
- inputHistoryDraftRef.current = inputRef.current;
1724
- const nextIndex = Math.min(history.length - 1, (currentIndex ?? -1) + 1);
1725
- inputHistoryIndexRef.current = nextIndex;
1726
- updateComposerInput(history[nextIndex] ?? '');
1727
- return;
1728
- }
1729
- if (currentIndex === null)
1730
- return;
1731
- const nextIndex = currentIndex - 1;
1732
- if (nextIndex < 0) {
1733
- inputHistoryIndexRef.current = null;
1734
- updateComposerInput(inputHistoryDraftRef.current);
1735
- return;
1736
- }
1737
- inputHistoryIndexRef.current = nextIndex;
1738
- updateComposerInput(history[nextIndex] ?? '');
1758
+ const transition = navigateComposerPromptHistory(promptHistoryRef.current, direction, createComposerEditor(inputRef.current, inputCursorRef.current));
1759
+ promptHistoryRef.current = transition.state;
1760
+ if (transition.editor !== null)
1761
+ updateComposerInput(transition.editor.text, transition.editor.cursor);
1739
1762
  };
1740
1763
  const withdrawLatestPending = () => {
1741
1764
  if (inputRef.current.trim().length !== 0)
@@ -2253,10 +2276,12 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2253
2276
  const loadId = sessionLoadRef.current + 1;
2254
2277
  sessionLoadRef.current = loadId;
2255
2278
  if (nextSessionId === null) {
2279
+ promptHistoryRef.current = createComposerPromptHistory();
2256
2280
  setHistory([]);
2257
2281
  setSessionColor(undefined);
2258
2282
  return;
2259
2283
  }
2284
+ promptHistoryRef.current = createComposerPromptHistory();
2260
2285
  const loading = (async () => {
2261
2286
  try {
2262
2287
  const commands = await service();
@@ -2266,6 +2291,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2266
2291
  : await commands.agentColor(nextSessionId);
2267
2292
  if (sessionLoadRef.current === loadId) {
2268
2293
  setHistory(transcript ? [...transcript] : []);
2294
+ promptHistoryRef.current = seedComposerPromptHistory(transcriptPrompts(transcript ?? []));
2269
2295
  setSessionColor(agentColor);
2270
2296
  }
2271
2297
  }
@@ -2753,23 +2779,58 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2753
2779
  onTurnChange?.(loading);
2754
2780
  void loading.finally(() => onTurnChange?.(null));
2755
2781
  };
2756
- const changeWorkingDirectory = (requestedCwd) => {
2782
+ const changeWorkingDirectory = (requestedCwd, authorization = {
2783
+ kind: 'unapproved',
2784
+ }) => {
2757
2785
  const changing = (async () => {
2758
- setBusy(true);
2759
- setStatus('changing directory');
2760
2786
  try {
2761
2787
  const commands = await service();
2762
2788
  if (!commands.changeCwd) {
2763
2789
  throw new Error('This interactive service cannot change working directories.');
2764
2790
  }
2765
- const cwd = await commands.changeCwd(sessionId ?? undefined, requestedCwd);
2791
+ if (!commands.inspectCwd) {
2792
+ throw new Error('This interactive service cannot inspect or change working directories.');
2793
+ }
2794
+ const inspection = await commands.inspectCwd(requestedCwd);
2795
+ if (authorization.kind === 'approved' &&
2796
+ inspection.canonicalTarget !== authorization.canonicalTarget) {
2797
+ throw new Error(`Directory changed after approval: expected ${authorization.canonicalTarget}, resolved to ${inspection.canonicalTarget}. Review the target and try again.`);
2798
+ }
2799
+ const activeSessionTrust = trustedCwds();
2800
+ activeSessionTrust.add(inspection.canonicalCurrentCwd);
2801
+ if (inspection.sameDirectory) {
2802
+ append({
2803
+ kind: 'local-result',
2804
+ text: `Already in ${inspection.canonicalTarget}.`,
2805
+ });
2806
+ return;
2807
+ }
2808
+ if (authorization.kind === 'unapproved' &&
2809
+ !activeSessionTrust.has(inspection.canonicalTarget)) {
2810
+ const pending = {
2811
+ canonicalPath: inspection.canonicalTarget,
2812
+ };
2813
+ cdTrustRef.current = pending;
2814
+ setCdTrust(pending);
2815
+ return;
2816
+ }
2817
+ setBusy(true);
2818
+ setStatus('changing directory');
2819
+ const cwd = await commands.changeCwd(sessionId ?? undefined, inspection.canonicalTarget, inspection.canonicalTarget);
2820
+ activeSessionTrust.add(cwd);
2766
2821
  runtimeCwdRef.current = cwd;
2767
2822
  setRuntimeCwd(cwd);
2768
2823
  await retireService();
2769
2824
  append({ kind: 'local-result', text: `Moved to ${cwd}` });
2770
2825
  }
2771
2826
  catch (error) {
2772
- warn(error);
2827
+ const message = error instanceof Error ? error.message : String(error);
2828
+ if (authorization.kind === 'approved')
2829
+ trustedCwds().delete(authorization.canonicalTarget);
2830
+ if (/^(Could not find a directory at .*\.|.* is not a directory\. Did you mean .+\?)$/u.test(message))
2831
+ append({ kind: 'local-result', text: message });
2832
+ else
2833
+ warn(error);
2773
2834
  }
2774
2835
  finally {
2775
2836
  setBusy(false);
@@ -3249,6 +3310,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3249
3310
  setSessionId(restoredConversation.sessionId || null);
3250
3311
  setPendingFork(false);
3251
3312
  setHistory(restoredConversation.history);
3313
+ promptHistoryRef.current = seedComposerPromptHistory(transcriptPrompts(restoredConversation.history));
3252
3314
  updateComposerInput(point.prompt);
3253
3315
  }
3254
3316
  append({
@@ -3701,6 +3763,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3701
3763
  case 'command-palette':
3702
3764
  setCommandPaletteOpen(false);
3703
3765
  break;
3766
+ case 'cd-trust':
3767
+ cdTrustRef.current = null;
3768
+ setCdTrust(null);
3769
+ setCdTrustSelection(0);
3770
+ break;
3704
3771
  default: {
3705
3772
  const unhandledTarget = effect.target;
3706
3773
  return unhandledTarget;
@@ -3751,7 +3818,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3751
3818
  : ['Select']
3752
3819
  : commandPaletteVisible || filePickerVisible
3753
3820
  ? ['Autocomplete', 'Chat']
3754
- : permission || planApproval
3821
+ : permission || planApproval || cdTrust
3755
3822
  ? ['Confirmation']
3756
3823
  : selectingSession
3757
3824
  ? ['Select']
@@ -3822,6 +3889,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3822
3889
  pendingPrefix: hasPendingPrefix,
3823
3890
  permission: Boolean(permission),
3824
3891
  planApproval: Boolean(planApproval),
3892
+ cdTrust: Boolean(cdTrust),
3825
3893
  question: Boolean(question),
3826
3894
  elicitation: elicitation
3827
3895
  ? elicitationUrlWaiting
@@ -4076,6 +4144,35 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
4076
4144
  }
4077
4145
  return;
4078
4146
  }
4147
+ if (cdTrust) {
4148
+ const resolveCdTrust = (selectedIndex) => {
4149
+ const pending = cdTrustRef.current;
4150
+ cdTrustRef.current = null;
4151
+ setCdTrust(null);
4152
+ setCdTrustSelection(0);
4153
+ if (!pending || selectedIndex !== 1)
4154
+ return;
4155
+ changeWorkingDirectory(pending.canonicalPath, {
4156
+ kind: 'approved',
4157
+ canonicalTarget: pending.canonicalPath,
4158
+ });
4159
+ };
4160
+ if (key.upArrow) {
4161
+ setCdTrustSelection((current) => Math.max(0, current - 1));
4162
+ }
4163
+ else if (key.downArrow) {
4164
+ setCdTrustSelection((current) => Math.min(1, current + 1));
4165
+ }
4166
+ else if (lower === 'y')
4167
+ resolveCdTrust(1);
4168
+ else if (lower === 'n')
4169
+ resolveCdTrust(0);
4170
+ else if (/^[12]$/u.test(value))
4171
+ resolveCdTrust(Number(value) - 1);
4172
+ else if (key.return)
4173
+ resolveCdTrust(cdTrustSelection);
4174
+ return;
4175
+ }
4079
4176
  if (planApproval) {
4080
4177
  const elevatedMode = runtimeSettingsRef.current
4081
4178
  .useAutoModeDuringPlan
@@ -6285,9 +6382,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
6285
6382
  streamingFrameRef.current?.flush();
6286
6383
  setThinkingExpanded(false);
6287
6384
  setStatus('ready');
6288
- inputHistoryRef.current = [];
6289
- inputHistoryIndexRef.current = null;
6290
- inputHistoryDraftRef.current = '';
6291
6385
  })().catch(warn);
6292
6386
  }
6293
6387
  else if (prompt === '/model') {
@@ -0,0 +1,17 @@
1
+ import { type ComposerEditorState } from './composer-editor.js';
2
+ export declare const COMPOSER_PROMPT_HISTORY_LIMIT = 100;
3
+ export interface ComposerPromptHistoryState {
4
+ readonly entries: readonly string[];
5
+ readonly index: number | null;
6
+ readonly draft: ComposerEditorState | null;
7
+ }
8
+ export interface ComposerPromptHistoryTransition {
9
+ readonly state: ComposerPromptHistoryState;
10
+ readonly editor: ComposerEditorState | null;
11
+ }
12
+ export declare function createComposerPromptHistory(): ComposerPromptHistoryState;
13
+ export declare function seedComposerPromptHistory(prompts: readonly string[]): ComposerPromptHistoryState;
14
+ export declare function recordComposerPrompt(state: ComposerPromptHistoryState, prompt: string): ComposerPromptHistoryState;
15
+ export declare function navigateComposerPromptHistory(state: ComposerPromptHistoryState, direction: 'previous' | 'next', currentEditor: ComposerEditorState): ComposerPromptHistoryTransition;
16
+ export declare function resetComposerPromptHistoryNavigation(state: ComposerPromptHistoryState): ComposerPromptHistoryState;
17
+ //# sourceMappingURL=composer-prompt-history.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { createComposerEditor, } from './composer-editor.js';
2
+ export const COMPOSER_PROMPT_HISTORY_LIMIT = 100;
3
+ export function createComposerPromptHistory() {
4
+ return { entries: [], index: null, draft: null };
5
+ }
6
+ function normalize(prompts) {
7
+ const entries = [];
8
+ for (const prompt of prompts) {
9
+ if (prompt.trim() === '')
10
+ continue;
11
+ if (entries.at(-1) === prompt)
12
+ continue;
13
+ entries.push(prompt);
14
+ }
15
+ return entries.slice(-COMPOSER_PROMPT_HISTORY_LIMIT);
16
+ }
17
+ export function seedComposerPromptHistory(prompts) {
18
+ return { entries: normalize(prompts), index: null, draft: null };
19
+ }
20
+ export function recordComposerPrompt(state, prompt) {
21
+ if (prompt.trim() === '')
22
+ return state;
23
+ return {
24
+ entries: normalize([...state.entries, prompt]),
25
+ index: null,
26
+ draft: null,
27
+ };
28
+ }
29
+ export function navigateComposerPromptHistory(state, direction, currentEditor) {
30
+ if (state.entries.length === 0)
31
+ return { state, editor: null };
32
+ if (direction === 'previous') {
33
+ const index = state.index === null
34
+ ? state.entries.length - 1
35
+ : Math.max(0, state.index - 1);
36
+ const draft = state.index === null
37
+ ? createComposerEditor(currentEditor.text, currentEditor.cursor)
38
+ : state.draft;
39
+ const nextState = { ...state, index, draft };
40
+ return {
41
+ state: nextState,
42
+ editor: createComposerEditor(state.entries[index]),
43
+ };
44
+ }
45
+ if (state.index === null)
46
+ return { state, editor: null };
47
+ if (state.index < state.entries.length - 1) {
48
+ const index = state.index + 1;
49
+ return {
50
+ state: { ...state, index },
51
+ editor: createComposerEditor(state.entries[index]),
52
+ };
53
+ }
54
+ const editor = state.draft;
55
+ return {
56
+ state: { ...state, index: null, draft: null },
57
+ editor,
58
+ };
59
+ }
60
+ export function resetComposerPromptHistoryNavigation(state) {
61
+ return { ...state, index: null, draft: null };
62
+ }
63
+ //# sourceMappingURL=composer-prompt-history.js.map
@@ -32,6 +32,10 @@ export type TuiDecisionSurfaceInput = {
32
32
  readonly questions: readonly ClaudeQuestion[];
33
33
  readonly questionIndex: number;
34
34
  readonly answer: string;
35
+ } | {
36
+ readonly kind: 'cd-trust';
37
+ readonly canonicalPath: string;
38
+ readonly selectedIndex: number;
35
39
  };
36
40
  export interface TuiPlanApprovalSurfaceModel {
37
41
  readonly kind: 'plan-approval';
@@ -66,6 +70,19 @@ export interface TuiQuestionSurfaceModel {
66
70
  readonly cancellation: TuiDecisionSurfaceCancellation;
67
71
  readonly emptyState?: string;
68
72
  }
69
- export type TuiDecisionSurfaceModel = TuiPlanApprovalSurfaceModel | TuiQuestionSurfaceModel;
73
+ export interface TuiCdTrustSurfaceModel {
74
+ readonly kind: 'cd-trust';
75
+ readonly heading: 'Moving to a new directory:';
76
+ readonly canonicalPath: string;
77
+ readonly explanation: "This session hasn't worked here before. Is this a directory you created or one you trust?";
78
+ readonly scope: 'Praxis can read, edit, and execute files in this directory.';
79
+ readonly securityGuide: 'Security guide: https://code.claude.com/docs/en/security';
80
+ readonly options: readonly TuiDecisionSurfaceOption[];
81
+ readonly selectedIndex: number;
82
+ readonly range: TuiDecisionSurfaceRange;
83
+ readonly actions: readonly TuiDecisionSurfaceAction[];
84
+ readonly cancellation: TuiDecisionSurfaceCancellation;
85
+ }
86
+ export type TuiDecisionSurfaceModel = TuiPlanApprovalSurfaceModel | TuiQuestionSurfaceModel | TuiCdTrustSurfaceModel;
70
87
  export declare function projectTuiDecisionSurface(input: TuiDecisionSurfaceInput): TuiDecisionSurfaceModel;
71
88
  //# sourceMappingURL=decision-surface-model.d.ts.map