praxis-agent 0.44.0 → 0.45.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
@@ -101,7 +101,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
101
101
  ## What Praxis provides
102
102
 
103
103
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
104
- `you>`/`praxis>` conversation, compact stable tool rows, responsive density,
104
+ `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
105
+ composer grammar, compact stable tool rows, responsive density,
105
106
  terminal-native background, and a minimal composer/status row. Interactive
106
107
  surfaces share the same presentation across terminals, with English
107
108
  permission/configuration choices and a taught `❯` / Up/Down / Enter / Esc
@@ -139,8 +140,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
139
140
  provider-free `/terminal-setup` diagnostics and repeatable Shift+Enter setup
140
141
  for supported local terminals,
141
142
  `Ctrl+V` text/image clipboard paste, `Ctrl+Z` shell suspension and `fg`
142
- recovery, permission-gated `!` shell turns, navigable current/per-turn Git
143
- diff views, semantic plan/question decision panels with complete
143
+ recovery, permission-gated provider-free `!` shell turns that persist shell
144
+ input/output for later ordinary prompts without creating an assistant turn,
145
+ navigable current/per-turn Git diff views, semantic plan/question decision panels with complete
144
146
  screen-reader actions, semantic screen projection across selectable surfaces,
145
147
  deterministic resize-aware URL/form elicitation rendering, and measured
146
148
  context budgets; print mode,
@@ -210,6 +212,22 @@ Detailed feature status and executable evidence live in the
210
212
  [parity matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md),
211
213
  not in this entry-point README.
212
214
 
215
+ ## Native data plane
216
+
217
+ Praxis defaults to an independent local native data plane:
218
+
219
+ ```text
220
+ Praxis ─── ~/.praxis (or PRAXIS_HOME)
221
+ ```
222
+
223
+ All sessions, memory, tasks, scheduled tasks, resources, and private state live
224
+ under `~/.praxis` (or `PRAXIS_HOME`). The authoritative transcript is
225
+ append-only `praxis.transcript` v1 JSONL; legacy Claude transcripts, indexes,
226
+ sidechains, and migration/recovery paths have been removed. `CLAUDE_CONFIG_DIR`
227
+ does not participate in native runs, and legacy directories are neither read
228
+ nor written. Claude-shaped messages and tool fields describe protocol shape
229
+ only; they do not change Praxis data ownership.
230
+
213
231
  ## Documentation
214
232
 
215
233
  | Need | Document |
@@ -2592,7 +2592,9 @@ export class ClaudeSessionService {
2592
2592
  });
2593
2593
  }
2594
2594
  const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
2595
- if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
2595
+ if (shellCommand === undefined &&
2596
+ this.options.maxBudgetUsd !== undefined &&
2597
+ !initialPricing) {
2596
2598
  throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
2597
2599
  }
2598
2600
  const shellInputUuid = shellCommand === undefined ? null : randomUUID();
@@ -3699,7 +3701,9 @@ export class ClaudeSessionService {
3699
3701
  };
3700
3702
  },
3701
3703
  });
3702
- await contextEngine.prepare(contextTransitionPort(pendingUserMessages), signal);
3704
+ if (shellCommand === undefined) {
3705
+ await contextEngine.prepare(contextTransitionPort(pendingUserMessages), signal);
3706
+ }
3703
3707
  for (const [index, message] of expandedMessages.entries()) {
3704
3708
  if (shellCommand !== undefined)
3705
3709
  break;
@@ -3783,6 +3787,9 @@ export class ClaudeSessionService {
3783
3787
  }
3784
3788
  let shellUsage = { inputTokens: 0, outputTokens: 0 };
3785
3789
  let shellModelUsage;
3790
+ let shellDurationApiMs;
3791
+ let shellDurationApiWithoutRetriesMs;
3792
+ let shellToolDurationMs;
3786
3793
  if (shellCommand !== undefined) {
3787
3794
  const call = {
3788
3795
  id: `shell_${randomUUID().replaceAll('-', '')}`,
@@ -3822,6 +3829,10 @@ export class ClaudeSessionService {
3822
3829
  throw error;
3823
3830
  }
3824
3831
  shellUsage = shellResult.usage ?? shellUsage;
3832
+ shellDurationApiMs = shellResult.durationApiMs;
3833
+ shellDurationApiWithoutRetriesMs =
3834
+ shellResult.durationApiWithoutRetriesMs;
3835
+ shellToolDurationMs = shellResult.durationToolMs;
3825
3836
  if (!shellResult.isError) {
3826
3837
  shellModelUsage = shellResult.modelUsage;
3827
3838
  foregroundLineChanges.add(shellResult);
@@ -3872,7 +3883,74 @@ export class ClaudeSessionService {
3872
3883
  isError: shellResult.isError,
3873
3884
  });
3874
3885
  }
3875
- if (budget) {
3886
+ if (shellCommand !== undefined) {
3887
+ const tracker = this.sessionCostTrackers.get(sessionId);
3888
+ if (!tracker) {
3889
+ throw new Error(`Session cost tracker is not active for session ${sessionId}`);
3890
+ }
3891
+ const totalUsage = mergeUsage(recoveryUsage, shellUsage);
3892
+ const turnModelUsage = mergeSessionRawModelUsage(recoveryModelUsage, shellModelUsage);
3893
+ let rawCostUsd;
3894
+ if (turnModelUsage) {
3895
+ for (const [model, usage] of Object.entries(turnModelUsage)) {
3896
+ const pricing = this.options.pricing?.resolve(model);
3897
+ const costUsd = pricing
3898
+ ? usageCostUsd(usage, pricing)
3899
+ : undefined;
3900
+ if (costUsd !== undefined)
3901
+ rawCostUsd = (rawCostUsd ?? 0) + costUsd;
3902
+ tracker.recordTurn({
3903
+ model,
3904
+ usage,
3905
+ ...(costUsd === undefined ? {} : { costUsd }),
3906
+ ...(usage.webSearchRequests === undefined
3907
+ ? {}
3908
+ : { webSearchRequests: usage.webSearchRequests }),
3909
+ });
3910
+ }
3911
+ }
3912
+ let combinedToolDurationMs = 0;
3913
+ for (const recoveryResult of recoveryResults) {
3914
+ combinedToolDurationMs = addToolDuration(recoveryResult.durationToolMs, combinedToolDurationMs);
3915
+ }
3916
+ combinedToolDurationMs = addToolDuration(shellToolDurationMs, combinedToolDurationMs);
3917
+ tracker.recordDurations({
3918
+ ...(shellDurationApiMs === undefined
3919
+ ? {}
3920
+ : { apiDurationMs: shellDurationApiMs }),
3921
+ ...(shellDurationApiWithoutRetriesMs === undefined
3922
+ ? {}
3923
+ : {
3924
+ apiDurationWithoutRetriesMs: shellDurationApiWithoutRetriesMs,
3925
+ }),
3926
+ ...(combinedToolDurationMs === 0
3927
+ ? {}
3928
+ : { toolDurationMs: combinedToolDurationMs }),
3929
+ });
3930
+ if (foregroundLineChanges.linesAdded !== 0 ||
3931
+ foregroundLineChanges.linesRemoved !== 0) {
3932
+ tracker.recordLineChanges({
3933
+ ...(foregroundLineChanges.linesAdded === 0
3934
+ ? {}
3935
+ : { linesAdded: foregroundLineChanges.linesAdded }),
3936
+ ...(foregroundLineChanges.linesRemoved === 0
3937
+ ? {}
3938
+ : { linesRemoved: foregroundLineChanges.linesRemoved }),
3939
+ });
3940
+ }
3941
+ turnCompleted = true;
3942
+ return {
3943
+ sessionId,
3944
+ text: '',
3945
+ usage: totalUsage,
3946
+ ...(rawCostUsd === undefined ? {} : { costUsd: rawCostUsd }),
3947
+ ...(turnModelUsage ? { modelUsage: { ...turnModelUsage } } : {}),
3948
+ ...(shellDurationApiMs === undefined
3949
+ ? {}
3950
+ : { durationApiMs: shellDurationApiMs }),
3951
+ };
3952
+ }
3953
+ if (shellCommand === undefined && budget) {
3876
3954
  await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
3877
3955
  budget.assertFits(budget.evaluate([
3878
3956
  ...contextMessages,
@@ -3911,7 +3989,9 @@ export class ClaudeSessionService {
3911
3989
  ];
3912
3990
  }
3913
3991
  await refreshRuntimeContext?.();
3914
- await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
3992
+ if (shellCommand === undefined) {
3993
+ await contextEngine.prepare(contextTransitionPort([], currentTurnUserMessages ?? []), signal);
3994
+ }
3915
3995
  runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
3916
3996
  return [
3917
3997
  ...contextMessages,
@@ -3240,7 +3240,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3240
3240
  streamingFrameRef.current?.resetText();
3241
3241
  streamingFrameRef.current?.resetThinking();
3242
3242
  streamingFrameRef.current?.flush();
3243
- append({ kind: 'assistant', text: result.text });
3243
+ if (shellCommand === undefined) {
3244
+ append({ kind: 'assistant', text: result.text });
3245
+ }
3244
3246
  if (turnMutatedFilesRef.current) {
3245
3247
  try {
3246
3248
  const snapshot = await loadDiffSnapshot();
@@ -272,18 +272,18 @@ export function projectQuietFrame(input) {
272
272
  : cleanActiveTail(body.transcript.active.text, viewportRows === undefined ? 200 : Math.max(1, viewportRows - 2));
273
273
  const activeText = activeTail.lines;
274
274
  if (activeText.some(({ text }) => Boolean(text)))
275
- activeText.forEach(({ text: part, start }, index) => lines.push(createQuietFrameRow(`quiet:active:text:${start}`, `${index === 0 ? 'praxis> ' : ' '}${index === 0 && activeTail.hidden ? '… ' : ''}${part}`, 'active', 'body', `${index === 0 ? 'Praxis: ' : ''}${part}`)));
275
+ activeText.forEach(({ text: part, start }, index) => lines.push(createQuietFrameRow(`quiet:active:text:${start}`, `${index === 0 ? ' ' : ' '}${index === 0 && activeTail.hidden ? '… ' : ''}${part}`, 'active', 'body', `${index === 0 ? 'Praxis: ' : ''}${part}`)));
276
276
  const thinking = screenReader
277
277
  ? cleanActiveTail(body.transcript.active.thinking, Number.POSITIVE_INFINITY)
278
278
  : cleanActiveTail(body.transcript.active.thinking, viewportRows === undefined ? 200 : Math.max(1, viewportRows - 2));
279
279
  if (thinking.lines.some(({ text }) => Boolean(text)))
280
- thinking.lines.forEach(({ text: part, start }, index) => lines.push(createQuietFrameRow(`quiet:active:thinking:${start}`, `${index === 0 ? ' ' : ' '}${part}`, 'active', 'muted', `${index === 0 ? 'Thinking: ' : ''}${part}`)));
280
+ thinking.lines.forEach(({ text: part, start }, index) => lines.push(createQuietFrameRow(`quiet:active:thinking:${start}`, `${index === 0 ? ' ' : ' '}${part}`, 'active', 'muted', `${index === 0 ? 'Thinking: ' : ''}${part}`)));
281
281
  }
282
282
  }
283
283
  const focus = input.focusRows.length > 0
284
284
  ? input.focusRows.map((item) => withRegion(item, 'focus'))
285
285
  : [
286
- createQuietFrameRow('quiet:composer', `${input.shellMode ? 'shell>' : 'you>'} ${clean(input.composerText)}`, 'composer', 'input', `${input.shellMode ? 'Shell' : 'Composer'} input field`),
286
+ createQuietFrameRow('quiet:composer', `${input.shellMode ? '! ' : ''}${clean(input.composerText)}`, 'composer', 'input', `${input.shellMode ? 'Shell' : 'Composer'} input field`),
287
287
  ];
288
288
  lines.push(...focus);
289
289
  const statusText = `${input.busy ? 'Working · ' : ''}${clean(input.status) || (input.busy ? 'In progress' : 'Ready')}`;
@@ -302,7 +302,7 @@ export function projectQuietFrame(input) {
302
302
  const compact = [...retained, ...focusRows, status];
303
303
  lines.splice(0, lines.length, ...compact);
304
304
  }
305
- const composerPrefix = `${input.shellMode ? 'shell>' : 'you>'} `;
305
+ const composerPrefix = input.shellMode ? '! ' : '';
306
306
  if (!screenReader) {
307
307
  for (let index = 0; index < lines.length; index += 1) {
308
308
  const row = lines[index];
@@ -497,7 +497,7 @@ function estimateRenderedTranscriptEntryLines(entry, width, mode) {
497
497
  if (item.kind === 'user')
498
498
  return (1 +
499
499
  (mode === 'screen-reader' ? 1 : 0) +
500
- wrappedLineCount(`${mode === 'screen-reader' ? 'You: ' : 'you> '}${item.text}`, width));
500
+ wrappedLineCount(`${mode === 'screen-reader' ? 'You: ' : ' '}${item.text}`, width));
501
501
  if (item.kind === 'assistant')
502
502
  return (1 +
503
503
  (mode === 'screen-reader' ? 1 : 0) +
@@ -667,7 +667,7 @@ function entryViewportRows(entry, width, mode) {
667
667
  return [
668
668
  '',
669
669
  ...(mode === 'screen-reader' ? [''] : []),
670
- ...textVisualRows(`${mode === 'screen-reader' ? 'You: ' : 'you> '}${item.text}`, width),
670
+ ...textVisualRows(`${mode === 'screen-reader' ? 'You: ' : ' '}${item.text}`, width),
671
671
  ];
672
672
  if (item.kind === 'assistant')
673
673
  return [
@@ -818,7 +818,7 @@ function assistantMarkdownProjectionRows(text, width, mode) {
818
818
  fenceAfter: false,
819
819
  fenceLabel: 'code',
820
820
  };
821
- const prefixed = textVisualRows(`praxis> ${first.text.trimEnd()}`, width).map((rowText) => ({ ...first, text: rowText }));
821
+ const prefixed = textVisualRows(`⏺ ${first.text.trimEnd()}`, width).map((rowText) => ({ ...first, text: rowText }));
822
822
  return [...prefixed, ...rows.slice(1)];
823
823
  }
824
824
  export function createTranscriptEntryViewportIndex(entry, width, mode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",