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 +25 -18
- package/dist/application/session-service.d.ts +7 -1
- package/dist/application/session-service.js +42 -9
- package/dist/cli/interactive.d.ts +3 -2
- package/dist/cli/interactive.js +145 -51
- package/dist/cli/tui/composer-prompt-history.d.ts +17 -0
- package/dist/cli/tui/composer-prompt-history.js +63 -0
- package/dist/cli/tui/decision-surface-model.d.ts +18 -1
- package/dist/cli/tui/decision-surface-model.js +26 -0
- package/dist/cli/tui/decision-surface.js +4 -1
- package/dist/cli/tui/open-url.d.ts +11 -1
- package/dist/cli/tui/open-url.js +27 -7
- package/dist/cli/tui/quiet-choice-rows.js +14 -1
- package/dist/cli/tui/sandbox-settings.d.ts +10 -1
- package/dist/cli/tui/sandbox-settings.js +7 -7
- package/dist/cli/tui/tui-focus-stack.d.ts +1 -0
- package/dist/cli/tui/tui-focus-stack.js +6 -0
- package/dist/cli/tui/tui-interaction-router.d.ts +1 -1
- package/dist/cli/tui/tui-interaction-router.js +2 -1
- package/dist/cli-runtime.d.ts +3 -2
- package/dist/cli-runtime.js +2 -1
- package/dist/native/schema.d.ts +0 -4
- package/dist/native/schema.js +1 -23
- package/dist/tools/team-lead-tools.js +24 -5
- package/package.json +8 -5
- package/dist/native/fork.d.ts +0 -9
- package/dist/native/fork.js +0 -260
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
|
-
|
|
216
|
-
[
|
|
217
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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>;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -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
|
|
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
|
-
:
|
|
1131
|
-
? { priority:
|
|
1132
|
-
:
|
|
1133
|
-
? {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
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
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|