praxis-agent 0.45.3 → 0.46.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
@@ -106,7 +106,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
106
106
  terminal-native background, and a minimal composer/status row. Interactive
107
107
  surfaces share the same presentation across terminals, with English
108
108
  permission/configuration choices and a taught `❯` / Up/Down / Enter / Esc
109
- interaction grammar, plus a
109
+ interaction grammar. While a regular turn is active, the composer remains
110
+ editable: Enter steers at the next safe continuation boundary, Tab or
111
+ Alt+Enter queues a sequential follow-up turn, and pending input stays visible
112
+ and can be withdrawn with Up. The TUI also includes a
110
113
  shared-command
111
114
  slash palette, tabbed help and shortcut surfaces, searchable resume picker,
112
115
  restored active-branch conversation history, streaming and expandable
@@ -126,7 +129,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
126
129
  support, and no-color output,
127
130
  cursor/history composer, provider-free `/cost` usage and pricing summaries,
128
131
  with a hermetic PTY smoke covering real `runInteractive` ANSI entry,
129
- resize-safe lifecycle, and Ctrl-C restoration,
132
+ resize-safe lifecycle, Ctrl-C restoration, fullscreen `Ctrl+L` redraw, and
133
+ mouse-wheel/drag selection with edge autoscroll and OSC 52 copy,
130
134
  interactive `/doctor` diagnostics, per-session model/effort/permission controls,
131
135
  context/status/skill/task dashboards, prompt stash and continuation shortcuts,
132
136
  filterable `@` file and agent references, composer undo, `Ctrl+G` external
@@ -9,6 +9,7 @@ import { type ClaudeInterruptionClassification } from '../native/interruption.js
9
9
  import { type ModelDocument, type ModelImage, type ModelToolCall, type ModelProvider, type ModelUsage, type PermissionApproval, type PermissionDecision, type PermissionResolver, type PermissionUpdate, type RuntimeEventSink, type ToolRegistry } from '../core/runtime.js';
10
10
  import { type BackgroundTaskSnapshot } from './background-task-runtime.js';
11
11
  import type { ModelPricingRegistry } from '../core/usage.js';
12
+ import { type ActiveTurnInputCommandResult } from '../core/active-turn-input.js';
12
13
  import type { Compactor } from '../core/compaction.js';
13
14
  import { ContextBudget } from '../core/context-budget.js';
14
15
  import { type ContextAssembler } from '../core/context.js';
@@ -227,6 +228,7 @@ export declare class ClaudeSessionService {
227
228
  private readonly hookLifecycle;
228
229
  private readonly leadOperations;
229
230
  private readonly fileChangeWatcher;
231
+ private readonly activeTurnInputs;
230
232
  private runtimeCwd;
231
233
  constructor(options: ClaudeSessionServiceOptions);
232
234
  nextScheduledPrompt(signal?: AbortSignal): Promise<ScheduledPrompt | null>;
@@ -270,6 +272,8 @@ export declare class ClaudeSessionService {
270
272
  run(prompt: string, signal?: AbortSignal, sessionId?: string, name?: string, images?: readonly ModelImage[], documents?: readonly ModelDocument[]): Promise<SessionRunResult>;
271
273
  runShell(command: string, signal?: AbortSignal, sessionId?: string, name?: string): Promise<SessionRunResult>;
272
274
  resume(sessionId: string, prompt: string, signal?: AbortSignal, name?: string, images?: readonly ModelImage[], documents?: readonly ModelDocument[], resumeSessionAt?: string): Promise<SessionRunResult>;
275
+ steer(sessionId: string, content: string): ActiveTurnInputCommandResult;
276
+ withdrawSteering(sessionId: string, id: string): ActiveTurnInputCommandResult;
273
277
  resumeShell(sessionId: string, command: string, signal?: AbortSignal, name?: string, resumeSessionAt?: string): Promise<SessionRunResult>;
274
278
  answerSideQuestion(sessionId: string | undefined, question: string, signal?: AbortSignal, onDelta?: (delta: string) => void, permissionMode?: ClaudePermissionMode): Promise<SideQuestionResult>;
275
279
  forkSideQuestion(sessionId: string, question: string, signal?: AbortSignal): Promise<SideQuestionForkResult>;
@@ -21,6 +21,7 @@ import { BackgroundTaskRuntime, } from './background-task-runtime.js';
21
21
  import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
22
22
  import { usageCostUsd } from '../core/usage.js';
23
23
  import { isSessionId } from '../core/session.js';
24
+ import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
24
25
  import { ContextBudget, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
25
26
  import { ContextEngine } from './context-engine.js';
26
27
  import { TurnMemoryCoordinator } from './turn-memory-coordinator.js';
@@ -682,6 +683,7 @@ export class ClaudeSessionService {
682
683
  hookLifecycle;
683
684
  leadOperations;
684
685
  fileChangeWatcher;
686
+ activeTurnInputs = new Map();
685
687
  runtimeCwd;
686
688
  constructor(options) {
687
689
  const dataPlane = options.dataPlane ?? 'native';
@@ -887,6 +889,18 @@ export class ClaudeSessionService {
887
889
  }
888
890
  async close() {
889
891
  this.closing = true;
892
+ for (const { mailbox } of this.activeTurnInputs.values()) {
893
+ if (!mailbox)
894
+ continue;
895
+ for (const item of mailbox.close()) {
896
+ this.options.eventSink?.({
897
+ type: 'user-input-rejected',
898
+ id: item.id,
899
+ content: item.content,
900
+ reason: 'closed',
901
+ });
902
+ }
903
+ }
890
904
  await this.fileChangeWatcher?.close(5_000);
891
905
  await this.hookLifecycle.close();
892
906
  await this.drainDetachedHookRuns(5_000);
@@ -1299,6 +1313,30 @@ export class ClaudeSessionService {
1299
1313
  ...(signal ? { signal } : {}),
1300
1314
  });
1301
1315
  }
1316
+ steer(sessionId, content) {
1317
+ const active = this.activeTurnInputs.get(sessionId);
1318
+ if (!active)
1319
+ return { kind: 'no-active-turn' };
1320
+ const mailbox = active.mailbox;
1321
+ if (!mailbox)
1322
+ return { kind: 'not-steerable' };
1323
+ const result = mailbox.enqueue(content);
1324
+ if (result.kind === 'accepted')
1325
+ return result;
1326
+ if (result.kind === 'empty')
1327
+ return result;
1328
+ return { kind: 'turn-completing' };
1329
+ }
1330
+ withdrawSteering(sessionId, id) {
1331
+ const active = this.activeTurnInputs.get(sessionId);
1332
+ if (!active)
1333
+ return { kind: 'no-active-turn' };
1334
+ const mailbox = active.mailbox;
1335
+ if (!mailbox)
1336
+ return { kind: 'not-steerable' };
1337
+ const result = mailbox.withdraw(id);
1338
+ return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
1339
+ }
1302
1340
  async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
1303
1341
  this.worktreeManager?.bindSession(sessionId);
1304
1342
  return this.executeTurn({
@@ -2449,6 +2487,8 @@ export class ClaudeSessionService {
2449
2487
  const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
2450
2488
  const skipUserPrompt = submission.kind === 'retry';
2451
2489
  const controller = new TurnTerminalController(this.options.eventSink ?? (() => undefined));
2490
+ let activeTurnInput;
2491
+ let activeTurnRecord;
2452
2492
  try {
2453
2493
  this.assertTurnWritable();
2454
2494
  if (prompt.length === 0 && images.length === 0 && documents.length === 0)
@@ -2459,6 +2499,16 @@ export class ClaudeSessionService {
2459
2499
  if (shellCommand !== undefined && shellCommand.trim().length === 0) {
2460
2500
  throw new Error('Shell command must not be empty');
2461
2501
  }
2502
+ if (this.activeTurnInputs.has(sessionId)) {
2503
+ throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
2504
+ }
2505
+ activeTurnRecord =
2506
+ shellCommand === undefined
2507
+ ? {
2508
+ mailbox: (activeTurnInput = new ActiveTurnInputMailbox(randomUUID)),
2509
+ }
2510
+ : {};
2511
+ this.activeTurnInputs.set(sessionId, activeTurnRecord);
2462
2512
  await this.activateSessionCostTracker(sessionId);
2463
2513
  await this.ensureFileResources(sessionId, signal);
2464
2514
  this.worktreeManager?.bindSession(sessionId);
@@ -3213,6 +3263,24 @@ export class ClaudeSessionService {
3213
3263
  }
3214
3264
  await durableFollowUps.followUpUserMessagesCompleted(messages);
3215
3265
  },
3266
+ userInputDelivered: async (item) => {
3267
+ if (nativeLease) {
3268
+ await nativeLease.appendMessages({
3269
+ messages: [{ role: 'user', content: item.content }],
3270
+ });
3271
+ currentTurnUserMessages?.push(item.content);
3272
+ return;
3273
+ }
3274
+ const [steeringEntry] = translateProviderEvents([{ type: 'user-text-block', text: item.content }], this.translationContext(sessionId, snapshot));
3275
+ if (!steeringEntry)
3276
+ throw new Error('Could not translate steering message');
3277
+ const steeringTail = await this.append(lease, snapshot.tail, steeringEntry);
3278
+ snapshot = {
3279
+ entries: [...snapshot.entries, steeringEntry],
3280
+ tail: steeringTail,
3281
+ };
3282
+ currentTurnUserMessages?.push(item.content);
3283
+ },
3216
3284
  };
3217
3285
  let turnCompleted = false;
3218
3286
  try {
@@ -3974,6 +4042,7 @@ export class ClaudeSessionService {
3974
4042
  cwd: this.activeCwd(),
3975
4043
  toolResultDirectory,
3976
4044
  observer,
4045
+ ...(activeTurnInput ? { steering: activeTurnInput } : {}),
3977
4046
  ...(this.options.effort ? { effort: this.options.effort } : {}),
3978
4047
  ...(this.options.maxModelTurns !== undefined
3979
4048
  ? { maxModelTurns: this.options.maxModelTurns }
@@ -4373,6 +4442,20 @@ export class ClaudeSessionService {
4373
4442
  controller.fail(error, signal);
4374
4443
  throw error;
4375
4444
  }
4445
+ finally {
4446
+ if (activeTurnInput !== undefined) {
4447
+ for (const item of activeTurnInput.close()) {
4448
+ this.options.eventSink?.({
4449
+ type: 'user-input-rejected',
4450
+ id: item.id,
4451
+ content: item.content,
4452
+ reason: signal?.aborted ? 'cancelled' : 'failed',
4453
+ });
4454
+ }
4455
+ }
4456
+ if (this.activeTurnInputs.get(sessionId) === activeTurnRecord)
4457
+ this.activeTurnInputs.delete(sessionId);
4458
+ }
4376
4459
  }
4377
4460
  async ensureFileResources(sessionId, signal) {
4378
4461
  const resources = this.options.fileResources ?? [];
@@ -1,6 +1,7 @@
1
1
  import type { 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
+ import type { ActiveTurnInputCommandResult } from '../core/active-turn-input.js';
4
5
  import { type DataPlane } from '../persistence/data-plane.js';
5
6
  import type { CliElicitationRequest, CliElicitationResult, CliRuntimeInfo } from './protocol.js';
6
7
  import type { ClaudeInteractiveToolCallbacks } from '../tools/claude-interactive-tools.js';
@@ -34,6 +35,8 @@ import type { BackgroundTaskSnapshot } from '../application/background-task-runt
34
35
  interface InteractiveSessionCommands {
35
36
  run(prompt: string, signal?: AbortSignal, sessionId?: string, name?: string, images?: readonly ModelImage[]): Promise<SessionRunResult>;
36
37
  resume(sessionId: string, prompt: string, signal?: AbortSignal, name?: string, images?: readonly ModelImage[]): Promise<SessionRunResult>;
38
+ steer?(sessionId: string, content: string): ActiveTurnInputCommandResult;
39
+ withdrawSteering?(sessionId: string, id: string): ActiveTurnInputCommandResult;
37
40
  runShell?(command: string, signal?: AbortSignal, sessionId?: string, name?: string): Promise<SessionRunResult>;
38
41
  resumeShell?(sessionId: string, command: string, signal?: AbortSignal): Promise<SessionRunResult>;
39
42
  fork(sessionId: string, targetSessionId?: string, resumeSessionAt?: string): Promise<ForkResult>;