one-code-extension 0.4.0 → 0.5.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 +219 -219
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +66 -34
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +19 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's result and error texts for pi's built-in read, write and edit
|
|
3
|
+
* (pure). pi's tools keep their call shapes (`path`, `edits[]`) and their own
|
|
4
|
+
* behavior; only what the model reads back changes, by rewriting the first
|
|
5
|
+
* text block of a result whose text is pi's exact wording. Anything else
|
|
6
|
+
* (pi's truncation notes, an image, a block another hook appended) passes
|
|
7
|
+
* through untouched.
|
|
8
|
+
*
|
|
9
|
+
* The success texts say the file state is current, which is true here: the
|
|
10
|
+
* file-tracker observes the file after every successful write or edit, so the
|
|
11
|
+
* next edit is not refused as unread or stale.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type Block = { type: string; text?: string };
|
|
15
|
+
|
|
16
|
+
export const FILE_STATE_CURRENT = " (file state is current in your context — no need to read it back)";
|
|
17
|
+
|
|
18
|
+
/** Claude Code's missing-file error for read and edit. */
|
|
19
|
+
export function fileDoesNotExist(cwd: string): string {
|
|
20
|
+
return `File does not exist. Note: your current working directory is ${cwd}.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Claude Code's empty-file read result. */
|
|
24
|
+
export const EMPTY_FILE_WARNING = "<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>";
|
|
25
|
+
|
|
26
|
+
export interface FileToolResult {
|
|
27
|
+
toolName: string;
|
|
28
|
+
isError: boolean;
|
|
29
|
+
content: Block[];
|
|
30
|
+
/** The absolute path the tool touched. */
|
|
31
|
+
path: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
/** For a write: whether the file existed before the call (Claude Code says "updated" then, "created" otherwise). */
|
|
34
|
+
existedBefore?: boolean;
|
|
35
|
+
/** For a read: whether the file is empty on disk (asked only for an empty result). */
|
|
36
|
+
isEmptyFile?: () => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The first block's replacement text, or undefined to leave the result as pi wrote it. */
|
|
40
|
+
function rewrite(result: FileToolResult, text: string): string | undefined {
|
|
41
|
+
const { toolName, isError, path, cwd } = result;
|
|
42
|
+
if (toolName === "write" && !isError && text.startsWith("Successfully wrote to ")) {
|
|
43
|
+
return result.existedBefore
|
|
44
|
+
? `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`
|
|
45
|
+
: `File created successfully at: ${path}${FILE_STATE_CURRENT}`;
|
|
46
|
+
}
|
|
47
|
+
if (toolName === "edit" && !isError && /^Successfully replaced \d+ block\(s\) in /.test(text)) {
|
|
48
|
+
return `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`;
|
|
49
|
+
}
|
|
50
|
+
if (toolName === "edit" && isError && /^Could not edit file: .*\. Error code: ENOENT\.$/s.test(text)) return fileDoesNotExist(cwd);
|
|
51
|
+
if (toolName === "read" && isError) {
|
|
52
|
+
if (/^ENOENT: no such file or directory, \w+ '/.test(text)) return fileDoesNotExist(cwd);
|
|
53
|
+
if (text === "EISDIR: illegal operation on a directory, read") return `${text} '${path}'`;
|
|
54
|
+
}
|
|
55
|
+
if (toolName === "read" && !isError && text === "" && result.isEmptyFile?.()) return EMPTY_FILE_WARNING;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The result's content with Claude Code's text in place of pi's, or undefined when nothing changes. */
|
|
60
|
+
export function fileToolResultContent<B extends Block>(result: FileToolResult & { content: B[] }): B[] | undefined {
|
|
61
|
+
if (!Array.isArray(result.content)) return undefined;
|
|
62
|
+
const [first, ...rest] = result.content;
|
|
63
|
+
if (first?.type !== "text" || typeof first.text !== "string") return undefined;
|
|
64
|
+
const text = rewrite(result, first.text);
|
|
65
|
+
return text === undefined ? undefined : [{ ...first, text }, ...rest];
|
|
66
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files a shell command showed the model in full (pure apart from the bash
|
|
3
|
+
* grammar, which the caller awaits).
|
|
4
|
+
*
|
|
5
|
+
* The auto-mode note tells the model to read files with cat, head or sed, and
|
|
6
|
+
* GPT-6 models do. Before this, the edit that followed was refused as
|
|
7
|
+
* "not read in this conversation" although the whole file was in the
|
|
8
|
+
* transcript: 16 refused edits in one 72-run battery (2026-10-04). A file now
|
|
9
|
+
* counts as read when a line that runs a reader names it and the output holds
|
|
10
|
+
* the file's entire current content. That one check covers every way the
|
|
11
|
+
* model could have seen less: a pipe into `head`, `cat -n`, a slice, an output
|
|
12
|
+
* persisted or clipped for size, a write later in the same command. The
|
|
13
|
+
* stale-edit guard is unchanged, since the file is observed with that content.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { isAbsolute } from "node:path";
|
|
17
|
+
import { toAbsoluteBash } from "../auto-mode/paths.ts";
|
|
18
|
+
import { globComponentRegex, isUnknownTilde, movesDirectory, resolvePayload } from "../auto-mode/shell-analysis.ts";
|
|
19
|
+
import { LOOPS, parseCommand, type ParseResult, scopedTracker, type Segment, type Token } from "../auto-mode/shell-parse.ts";
|
|
20
|
+
|
|
21
|
+
/** Commands whose file arguments may print a whole file. */
|
|
22
|
+
const READERS = new Set(["cat", "head", "tail", "sed"]);
|
|
23
|
+
|
|
24
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
25
|
+
|
|
26
|
+
/** A word a reader may have printed, with the absolute directory the shell resolves it in. */
|
|
27
|
+
export interface ShellReadCandidate {
|
|
28
|
+
word: string;
|
|
29
|
+
dir: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The files `command` may have printed, as written: the plain arguments of
|
|
34
|
+
* its reader commands, plus every path-like word of a line that runs a reader
|
|
35
|
+
* at all. Candidates only, each still to be checked with {@link shownInFull}.
|
|
36
|
+
* Each comes with the directory its reader runs in, starting from `cwd` and
|
|
37
|
+
* following a literal `cd` ({@link segmentDirectories}); a reader whose
|
|
38
|
+
* directory cannot be known offers nothing. Empty when no reader runs or the
|
|
39
|
+
* command does not parse (the grammar not loaded included).
|
|
40
|
+
*/
|
|
41
|
+
export function shellReadCandidates(command: string, cwd: string, home: string): ShellReadCandidate[] {
|
|
42
|
+
const parsed = parseCommand(command);
|
|
43
|
+
if (parsed.parseFailed) return [];
|
|
44
|
+
const readers = parsed.segments.flatMap((segment, index) => {
|
|
45
|
+
const first = segment.tokens.findIndex((token) => !ASSIGNMENT.test(token.value));
|
|
46
|
+
if (first === -1) return [];
|
|
47
|
+
const [head, ...args] = segment.tokens.slice(first);
|
|
48
|
+
return head && !head.dynamic && READERS.has(head.value.split("/").pop() ?? "") ? [{ segment, index, args }] : [];
|
|
49
|
+
});
|
|
50
|
+
// Most commands run no reader: skip the directory walk for them.
|
|
51
|
+
if (readers.length === 0) return [];
|
|
52
|
+
const dirs = segmentDirectories(parsed, cwd, home);
|
|
53
|
+
const candidates = new Map<string, ShellReadCandidate>();
|
|
54
|
+
const add = (word: string, dir: string) => candidates.set(`${dir}\0${word}`, { word, dir });
|
|
55
|
+
const readerDirs = new Set<string>();
|
|
56
|
+
for (const { segment, index, args } of readers) {
|
|
57
|
+
const dir = dirs[index];
|
|
58
|
+
if (dir === undefined) continue;
|
|
59
|
+
readerDirs.add(dir);
|
|
60
|
+
if (segment.substitution !== undefined) continue;
|
|
61
|
+
for (const arg of args) if (isPlainPath(arg)) add(arg.value, dir);
|
|
62
|
+
}
|
|
63
|
+
// A reader fed from a variable (`for f in a.py b.py; do cat "$f"; done`)
|
|
64
|
+
// names its files elsewhere in the line. Every plain path-like word is a
|
|
65
|
+
// candidate then, in each directory a reader ran in: the full-content
|
|
66
|
+
// check, not the parse, decides.
|
|
67
|
+
if (readerDirs.size > 0) {
|
|
68
|
+
const words = new Set<string>();
|
|
69
|
+
for (const word of [...(command.match(PATH_WORD) ?? []), ...(command.match(GLOB_WORD) ?? [])]) if (words.size < MAX_CANDIDATES) words.add(word);
|
|
70
|
+
for (const dir of [...readerDirs].slice(0, MAX_READER_DIRS)) for (const word of words) add(word, dir);
|
|
71
|
+
}
|
|
72
|
+
return [...candidates.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The absolute paths `command` may have printed: its candidates
|
|
77
|
+
* ({@link shellReadCandidates}) resolved as the shell resolves them, from
|
|
78
|
+
* the directory each one's reader runs in, with a glob expanded through
|
|
79
|
+
* `readdir` (given an absolute directory). The shell's own resolution
|
|
80
|
+
* (`toAbsoluteBash`), not the file tools' (`resolveToolPath` strips a leading
|
|
81
|
+
* `@`, which `cat @x` keeps): `~/` and the shell's directory only.
|
|
82
|
+
*/
|
|
83
|
+
export function shellReadPaths(command: string, cwd: string, home: string, readdir: (dir: string) => string[]): string[] {
|
|
84
|
+
const paths = new Set<string>();
|
|
85
|
+
for (const { word, dir } of shellReadCandidates(command, cwd, home)) {
|
|
86
|
+
for (const name of expandCandidate(word, (sub) => readdir(toAbsoluteBash(dir, sub, home)))) paths.add(toAbsoluteBash(dir, name, home));
|
|
87
|
+
}
|
|
88
|
+
return [...paths];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The directory each segment runs in, or undefined where it cannot be known.
|
|
93
|
+
* Only a plain `cd <literal>` (or `cd -- <literal>`) that surely ran moves
|
|
94
|
+
* the commands after it in its own subshell scope (`scopedTracker`). One that
|
|
95
|
+
* may not run moves only the later members of its own `&&` list
|
|
96
|
+
* (`Segment.andList`), which run only once it succeeded (`cd src && cat x`
|
|
97
|
+
* inside the worktree's `cd <wt> && ( … )`). Anything else that may change
|
|
98
|
+
* the directory makes the commands after it unknown, never guessed: a
|
|
99
|
+
* dynamic, glob, `-` or `~user` target, an option, `pushd` and `popd`, a
|
|
100
|
+
* wrapper, assignment or `!` in front, a `cd` that may not run (an `if`
|
|
101
|
+
* branch, after `||`) for everything outside its `&&` list, a `cd` last in a
|
|
102
|
+
* pipeline (`lastpipe` runs it in the shell around), a backgrounded line,
|
|
103
|
+
* `eval` and `source`, and a line that defines a function (which may replace
|
|
104
|
+
* `cd`). A loop body runs more than once, so after a move in any loop every
|
|
105
|
+
* command in a loop is unknown too. A miss costs one re-read; a wrong
|
|
106
|
+
* directory would certify a read that never happened.
|
|
107
|
+
*/
|
|
108
|
+
function segmentDirectories(parsed: ParseResult, cwd: string, home: string): (string | undefined)[] {
|
|
109
|
+
const { segments } = parsed;
|
|
110
|
+
const dirs = scopedTracker<string | undefined>(cwd);
|
|
111
|
+
/** Where a `cd` that may not have run moved the later members of its `&&` list, by list. */
|
|
112
|
+
const listDirs = new Map<number, string | undefined>();
|
|
113
|
+
const inLoop = (segment: Segment) => segment.enclosing.some((construct) => LOOPS.has(construct));
|
|
114
|
+
const moves = segments.map(mayMove);
|
|
115
|
+
const loopMoves = segments.some((segment, index) => moves[index] && inLoop(segment));
|
|
116
|
+
return segments.map((segment, index) => {
|
|
117
|
+
const inList = segment.andList !== undefined && listDirs.has(segment.andList);
|
|
118
|
+
const dir = loopMoves && inLoop(segment) ? undefined : inList ? listDirs.get(segment.andList!) : dirs.get(segment);
|
|
119
|
+
if (!moves[index]) return dir;
|
|
120
|
+
const certain = !parsed.background && !parsed.definesFunction && !segment.pipelineShell && !segment.enclosing.includes("negated_command");
|
|
121
|
+
const target = certain ? literalCdTarget(segment) : undefined;
|
|
122
|
+
// A relative target from an unknown directory stays unknown; an absolute one re-anchors.
|
|
123
|
+
const next = target === undefined ? undefined : dir === undefined && !isAbsoluteTarget(target) ? undefined : toAbsoluteBash(dir ?? cwd, target, home);
|
|
124
|
+
if (segment.pipelineShell) dirs.set({ scopes: segment.pipelineShell }, undefined);
|
|
125
|
+
if (segment.conditional) {
|
|
126
|
+
dirs.set(segment, undefined);
|
|
127
|
+
if (segment.andList !== undefined) listDirs.set(segment.andList, next);
|
|
128
|
+
} else {
|
|
129
|
+
dirs.set(segment, next);
|
|
130
|
+
}
|
|
131
|
+
return dir;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether a segment may change the shell's directory: a `cd`/`pushd`/`popd` in any spelling, or a script run in this shell. */
|
|
136
|
+
function mayMove(segment: Segment): boolean {
|
|
137
|
+
return movesDirectory(segment) || ["eval", "source", "."].includes(resolvePayload(segment.tokens).command);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The target of `cd <word>` or `cd -- <word>` written plainly, or undefined for any other form. */
|
|
141
|
+
function literalCdTarget(segment: Segment): string | undefined {
|
|
142
|
+
const [head, ...args] = segment.tokens;
|
|
143
|
+
if (head?.value !== "cd" || head.dynamic) return undefined;
|
|
144
|
+
const rest = args[0]?.value === "--" ? args.slice(1) : args;
|
|
145
|
+
if (rest.length > 1) return undefined;
|
|
146
|
+
const target = rest[0];
|
|
147
|
+
// A bare `cd` goes home.
|
|
148
|
+
if (!target) return "~";
|
|
149
|
+
if (target.dynamic || target.glob || target.value === "" || target.value.startsWith("-") || isUnknownTilde(target.value)) return undefined;
|
|
150
|
+
return target.value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Whether a `cd` target lands in the same place from any directory. */
|
|
154
|
+
function isAbsoluteTarget(target: string): boolean {
|
|
155
|
+
return isAbsolute(target) || target === "~" || target.startsWith("~/");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The files a candidate names: itself, or for a glob in its last component
|
|
160
|
+
* (`src/*.py`, a loop's word list), the matching entries of that directory,
|
|
161
|
+
* listed through `readdir` (no shell). Globs in a directory component are not
|
|
162
|
+
* expanded.
|
|
163
|
+
*/
|
|
164
|
+
export function expandCandidate(word: string, readdir: (dir: string) => string[]): string[] {
|
|
165
|
+
if (!/[*?[]/.test(word)) return [word];
|
|
166
|
+
const slash = word.lastIndexOf("/");
|
|
167
|
+
const dir = slash === -1 ? "" : word.slice(0, slash);
|
|
168
|
+
if (/[*?[]/.test(dir)) return [];
|
|
169
|
+
const pattern = globComponentRegex(word.slice(slash + 1));
|
|
170
|
+
if (!pattern) return [];
|
|
171
|
+
let names: string[];
|
|
172
|
+
try {
|
|
173
|
+
names = readdir(dir || ".");
|
|
174
|
+
} catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return names.filter((name) => !name.startsWith(".") && pattern.test(name)).map((name) => (dir ? `${dir}/${name}` : name)).slice(0, MAX_CANDIDATES);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A word that could be a file path as written, absolute or relative: no quoting, expansion or glob characters. */
|
|
181
|
+
const PATH_WORD = /(?<![\w$\-/])\/?[\w.@+~][\w.@+~/,-]*/g;
|
|
182
|
+
/** A word with a glob character, which the shell would expand (`src/*.py`). */
|
|
183
|
+
const GLOB_WORD = /(?<![\w$\-])[\w.@+~/,-]*[*?[][\w.@+~/,*?[\]-]*/g;
|
|
184
|
+
const MAX_CANDIDATES = 50;
|
|
185
|
+
/** The path-like words of a line are tried in at most this many reader directories. */
|
|
186
|
+
const MAX_READER_DIRS = 4;
|
|
187
|
+
|
|
188
|
+
function isPlainPath(token: Token): boolean {
|
|
189
|
+
return !token.dynamic && !token.glob && token.value !== "" && !token.value.startsWith("-");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether `output` shows the whole of `content`. Trailing whitespace is
|
|
194
|
+
* ignored because the shell tool trims it. A tiny one-line file is never
|
|
195
|
+
* counted: its text turns up in outputs by chance (an adversarial GPT-6 Astra
|
|
196
|
+
* run matched `completed` to "(bash completed with no output)", `1` to
|
|
197
|
+
* "Exit code 1", an error line, a persisted-output tag), so the match would
|
|
198
|
+
* not show that the file was printed. What the guard protects still holds for
|
|
199
|
+
* a larger match under another file's name: every byte of the file was in
|
|
200
|
+
* front of the model, so an edit cannot discard content it has not seen.
|
|
201
|
+
*/
|
|
202
|
+
export function shownInFull(output: string, content: string): boolean {
|
|
203
|
+
const body = content.trimEnd();
|
|
204
|
+
if (body.length < MIN_SHOWN_CHARS && !(body.includes("\n") && body.length >= MIN_SHOWN_LINES_CHARS)) return false;
|
|
205
|
+
return output.includes(body);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A one-line file shorter than this is never counted as shown. */
|
|
209
|
+
const MIN_SHOWN_CHARS = 40;
|
|
210
|
+
/** A multi-line file shorter than this is never counted as shown. */
|
|
211
|
+
const MIN_SHOWN_LINES_CHARS = 16;
|
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
export type FreshnessStatus = "fresh" | "unread" | "stale" | "absent";
|
|
14
14
|
|
|
15
15
|
export interface TrackedFile {
|
|
16
|
-
/** Content as of the last read or write we performed. */
|
|
16
|
+
/** Content as of the last read or write we performed ("" when `oversized`). */
|
|
17
17
|
content: string;
|
|
18
|
+
/**
|
|
19
|
+
* Too large to keep: we saw the file but hold no content to compare. An
|
|
20
|
+
* explicit flag, never the empty string, so a file read while EMPTY
|
|
21
|
+
* compares like any other and cannot be overwritten after someone fills
|
|
22
|
+
* it (TOOLS-REVIEW-2026-09-26 M2).
|
|
23
|
+
*/
|
|
24
|
+
oversized: boolean;
|
|
18
25
|
at: number;
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -54,9 +61,9 @@ export class FileTracker {
|
|
|
54
61
|
this.notified.delete(path);
|
|
55
62
|
if (content.length > MAX_TRACKED_BYTES) {
|
|
56
63
|
// Too large to diff usefully; remember that we saw it, not its content.
|
|
57
|
-
this.files.set(path, { content: "", at });
|
|
64
|
+
this.files.set(path, { content: "", oversized: true, at });
|
|
58
65
|
} else {
|
|
59
|
-
this.files.set(path, { content, at });
|
|
66
|
+
this.files.set(path, { content, oversized: false, at });
|
|
60
67
|
}
|
|
61
68
|
if (stamp) this.stamps.set(path, stamp);
|
|
62
69
|
else this.stamps.delete(path);
|
|
@@ -103,8 +110,10 @@ export class FileTracker {
|
|
|
103
110
|
return this.files.has(path);
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
/** The content last seen, or undefined when untracked or tracked without content (oversized). */
|
|
106
114
|
lastSeen(path: string): string | undefined {
|
|
107
|
-
|
|
115
|
+
const known = this.files.get(path);
|
|
116
|
+
return known && !known.oversized ? known.content : undefined;
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
get tracked(): string[] {
|
|
@@ -123,7 +132,7 @@ export class FileTracker {
|
|
|
123
132
|
if (!known) return "unread";
|
|
124
133
|
// Oversized files are tracked without content; treat them as fresh rather
|
|
125
134
|
// than blocking edits we cannot reason about.
|
|
126
|
-
if (known.
|
|
135
|
+
if (known.oversized) return "fresh";
|
|
127
136
|
return known.content === currentContent ? "fresh" : "stale";
|
|
128
137
|
}
|
|
129
138
|
}
|
|
@@ -173,20 +182,28 @@ export function describeChanges(
|
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
export const UNREAD_REASON = (path: string, tool: string) =>
|
|
176
|
-
`Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen.`;
|
|
185
|
+
`Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen. A shell read counts only when it printed the whole file as it is on disk (a plain \`cat\`); after a partial one (head, sed -n, a pipe, or output cut off at the size limit) or another version of it (such as \`git show HEAD:<file>\`), use the read tool.`;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The same notice when one of this session's subagents or workflow agents
|
|
189
|
+
* changed the file (lib/child-writes.ts): saying "someone else, a formatter or
|
|
190
|
+
* a command" there sent the model looking for an outside cause.
|
|
191
|
+
*/
|
|
192
|
+
export const CHILD_CHANGE_REMINDER = (path: string, agent: string | undefined, excerpt: ChangeExcerpt) =>
|
|
193
|
+
`Note: ${path} was changed by ${agent ? `the agent "${agent}"` : "an agent"} you started in this session since you last read it. Take it as the current state. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
|
|
177
194
|
|
|
178
195
|
export const STALE_REASON = (path: string) =>
|
|
179
196
|
`${path} has changed on disk since you last read it — someone else, a formatter, or a command may have modified it. Read it again before editing, or your change would overwrite theirs.`;
|
|
180
197
|
|
|
181
198
|
/**
|
|
182
|
-
* Claude Code's `edited_text_file` attachment text,
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
199
|
+
* Claude Code's `edited_text_file` attachment text, plus One Code's closing
|
|
200
|
+
* clause telling the model to re-read before editing: Claude Code marks the
|
|
201
|
+
* file as read when it attaches the snippet, we deliberately do not (the
|
|
202
|
+
* stale-edit guard keeps firing), so without that clause the model's next
|
|
203
|
+
* edit is blocked and costs a round trip. The "take it as the current state
|
|
204
|
+
* rather than reverting it" steer is Claude Code's: without it a model
|
|
205
|
+
* mid-edit tends to restore its own version and narrate linter noise
|
|
206
|
+
* (STEERING-REVIEW-2026-09-05 M5).
|
|
190
207
|
*/
|
|
191
208
|
export const EXTERNAL_CHANGE_REMINDER = (path: string, excerpt: ChangeExcerpt) =>
|
|
192
|
-
`Note: ${path}
|
|
209
|
+
`Note: ${path} changed on disk since you last read it. That's usually deliberate, so take it as the current state rather than reverting it; if the change looks wrong, say so rather than undoing it yourself — otherwise no need to call it out. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* Everything here is pure and paint-injected so it unit-tests with an identity
|
|
8
8
|
* paint. `paint(color, text)` matches `safeThemePaint`; in tests it returns the
|
|
9
9
|
* text unchanged, so assertions see the plain line. Inclusion decisions use
|
|
10
|
-
* plain
|
|
11
|
-
*
|
|
10
|
+
* the plain text's terminal columns (`visibleWidth`, never `.length`, so a CJK
|
|
11
|
+
* path or branch counts two columns a glyph), so the painted result (ANSI adds
|
|
12
|
+
* zero display width) always fits the width it was built for.
|
|
12
13
|
*/
|
|
13
14
|
|
|
15
|
+
import { cutPlainText } from "../lib/tui-render.ts";
|
|
16
|
+
import { tailColumns, visibleWidth } from "../lib/text-width.ts";
|
|
14
17
|
import { costOf, usageEntryCost } from "../lib/usage-bus.ts";
|
|
15
18
|
import type { WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
16
19
|
|
|
@@ -21,8 +24,12 @@ export interface FooterData {
|
|
|
21
24
|
cwd: string;
|
|
22
25
|
home: string;
|
|
23
26
|
branch?: string;
|
|
24
|
-
/**
|
|
25
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Context tokens in use and the model's window. Tokens are unknown right
|
|
29
|
+
* after a compaction until the next reply; the fill then reads `?/window`,
|
|
30
|
+
* as pi's own footer shows it.
|
|
31
|
+
*/
|
|
32
|
+
contextTokens?: number | null;
|
|
26
33
|
contextWindow?: number;
|
|
27
34
|
/** Context percentage, shown in parens after the fill; `null`/undefined omits it
|
|
28
35
|
* (e.g. right after compaction, unknown until the next reply). */
|
|
@@ -73,12 +80,13 @@ function tilde(cwd: string, home: string): string {
|
|
|
73
80
|
|
|
74
81
|
/**
|
|
75
82
|
* Fit a path into `max` columns, keeping the tail (the deepest, most specific
|
|
76
|
-
* folders) with a leading `…`. `max <= 1` collapses to just `…`.
|
|
83
|
+
* folders) with a leading `…`. `max <= 1` collapses to just `…`. Cut by
|
|
84
|
+
* columns on a grapheme boundary, so a wide glyph or an emoji stays whole.
|
|
77
85
|
*/
|
|
78
86
|
function truncatePath(path: string, max: number): string {
|
|
79
|
-
if (path
|
|
87
|
+
if (visibleWidth(path) <= max) return path;
|
|
80
88
|
if (max <= 1) return "…";
|
|
81
|
-
return `…${
|
|
89
|
+
return `…${tailColumns(path, max - 1).text}`;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
interface Part {
|
|
@@ -86,6 +94,8 @@ interface Part {
|
|
|
86
94
|
painted: string;
|
|
87
95
|
/** Higher drops first when the line is too narrow; 0 = never dropped. */
|
|
88
96
|
dropPriority: number;
|
|
97
|
+
/** The model name: the one core part cut (with an ellipsis) when even the core overflows. */
|
|
98
|
+
model: boolean;
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
const SEP = " · ";
|
|
@@ -103,17 +113,19 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
|
|
|
103
113
|
// A labelled metric: the label is muted, the value takes `color`. The plain
|
|
104
114
|
// form (label + value) drives the width/drop math; the painted form colours
|
|
105
115
|
// the two halves separately.
|
|
106
|
-
const push = (label: string, value: string, color: string, dropPriority = 0) => {
|
|
116
|
+
const push = (label: string, value: string, color: string, dropPriority = 0, model = false) => {
|
|
107
117
|
if (!value) return;
|
|
108
118
|
const plain = label ? `${label} ${value}` : value;
|
|
109
119
|
const painted = label ? `${paint("muted", label)} ${paint(color, value)}` : paint(color, value);
|
|
110
|
-
parts.push({ plain, painted, dropPriority });
|
|
120
|
+
parts.push({ plain, painted, dropPriority, model });
|
|
111
121
|
};
|
|
112
122
|
|
|
113
|
-
// Context fill as "usage: tokens/window (pct%)"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
// Context fill as "usage: tokens/window (pct%)", or "?/window" while the
|
|
124
|
+
// tokens are unknown (right after a compaction).
|
|
125
|
+
if (typeof data.contextWindow === "number" && data.contextWindow > 0) {
|
|
126
|
+
const tokens = typeof data.contextTokens === "number" ? formatTokens(data.contextTokens) : "?";
|
|
127
|
+
const pct = typeof data.contextTokens === "number" && typeof data.contextPercent === "number" ? ` (${data.contextPercent.toFixed(0)}%)` : "";
|
|
128
|
+
push("usage:", `${tokens}/${formatTokens(data.contextWindow)}${pct}`, "dim");
|
|
117
129
|
}
|
|
118
130
|
// Total cost, always shown once known.
|
|
119
131
|
push("cost:", formatCost(data.cost), "dim");
|
|
@@ -123,7 +135,7 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
|
|
|
123
135
|
push("cache-hit:", `${data.cacheHitPercent.toFixed(0)}%`, color, 2);
|
|
124
136
|
}
|
|
125
137
|
// Model, then effort — no labels; both self-describing.
|
|
126
|
-
if (data.model) push("", data.model, "dim");
|
|
138
|
+
if (data.model) push("", data.model, "dim", 0, true);
|
|
127
139
|
if (data.effort) push("", data.effort, "accent");
|
|
128
140
|
|
|
129
141
|
// Left chunk: path + branch, with the open PR attached to the branch as
|
|
@@ -132,7 +144,7 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
|
|
|
132
144
|
// branch (bare-branch fallback), and only when there is no room even for the
|
|
133
145
|
// bare branch does the whole chunk go, leaving metrics alone. The three
|
|
134
146
|
// segments paint separately — path dim, branch in accent, PR in the link
|
|
135
|
-
// colour — but every width/gap decision uses their plain
|
|
147
|
+
// colour — but every width/gap decision uses their plain columns.
|
|
136
148
|
const path = tilde(data.cwd, data.home);
|
|
137
149
|
const branchSeg = data.branch ? ` ⎇ ${data.branch}` : "";
|
|
138
150
|
const prSeg = data.branch && typeof data.pr === "number" ? ` ← PR #${data.pr}` : "";
|
|
@@ -140,7 +152,7 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
|
|
|
140
152
|
// Drop optional right parts (highest priority first) until the metrics fit.
|
|
141
153
|
let kept = parts.slice();
|
|
142
154
|
const rightPlain = () => kept.map((p) => p.plain).join(SEP);
|
|
143
|
-
while (rightPlain()
|
|
155
|
+
while (visibleWidth(rightPlain()) > width) {
|
|
144
156
|
const droppable = kept
|
|
145
157
|
.map((p, i) => ({ p, i }))
|
|
146
158
|
.filter((x) => x.p.dropPriority > 0)
|
|
@@ -149,25 +161,34 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
|
|
|
149
161
|
kept = kept.filter((_, i) => i !== droppable.i);
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
164
|
+
// Even the core overflows: cut the model name with an ellipsis so the
|
|
165
|
+
// effort after it stays; when that is not enough, cut the whole line.
|
|
166
|
+
const overflow = visibleWidth(rightPlain()) - width;
|
|
167
|
+
if (overflow > 0) {
|
|
168
|
+
const model = kept.find((p) => p.model);
|
|
169
|
+
const room = model ? visibleWidth(model.plain) - overflow : 0;
|
|
170
|
+
if (model && room >= 4) {
|
|
171
|
+
const shortened = cutPlainText(model.plain, room);
|
|
172
|
+
kept = kept.map((p) => (p === model ? { ...p, plain: shortened, painted: paint("dim", shortened) } : p));
|
|
173
|
+
} else {
|
|
174
|
+
return [paint("dim", cutPlainText(rightPlain(), width))];
|
|
175
|
+
}
|
|
157
176
|
}
|
|
158
177
|
|
|
178
|
+
const rightLen = visibleWidth(rightPlain());
|
|
179
|
+
|
|
159
180
|
const rightPaintedStr = kept.map((p) => p.painted).join(paint("dim", SEP));
|
|
160
181
|
|
|
161
182
|
// Room for the left chunk after the metrics and at least one space of gap.
|
|
162
183
|
// Keep the PR only if the branch+PR both fit; otherwise drop it and keep the
|
|
163
184
|
// bare branch.
|
|
164
185
|
const room = width - rightLen - 1;
|
|
165
|
-
const keptPr = room >= branchSeg
|
|
166
|
-
const suffixLen = branchSeg
|
|
186
|
+
const keptPr = room >= visibleWidth(branchSeg) + visibleWidth(prSeg) + 1 ? prSeg : "";
|
|
187
|
+
const suffixLen = visibleWidth(branchSeg) + visibleWidth(keptPr);
|
|
167
188
|
if (room >= suffixLen + 1) {
|
|
168
189
|
const shownPath = truncatePath(path, room - suffixLen);
|
|
169
190
|
const leftPlain = `${shownPath}${branchSeg}${keptPr}`;
|
|
170
|
-
const gap = " ".repeat(width - leftPlain
|
|
191
|
+
const gap = " ".repeat(Math.max(0, width - visibleWidth(leftPlain) - rightLen));
|
|
171
192
|
const leftPainted = `${paint("dim", shownPath)}${paint("accent", branchSeg)}${paint("mdLink", keptPr)}`;
|
|
172
193
|
return [`${leftPainted}${gap}${rightPaintedStr}`];
|
|
173
194
|
}
|
|
@@ -24,7 +24,7 @@ import { ULTRACODE_STATUS_KEY } from "../effort/slider.ts";
|
|
|
24
24
|
import { USAGE_CHANNEL } from "../lib/usage-bus.ts";
|
|
25
25
|
import { buildFooterLines, computeMainUsage, footerLocation, type FooterData } from "./footer-line.ts";
|
|
26
26
|
import { WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
27
|
-
import { fetchPrNumber } from "./pr.ts";
|
|
27
|
+
import { fetchPrNumber, nextPrPollDelay, PR_SLOW_LOOKUP_MS } from "./pr.ts";
|
|
28
28
|
|
|
29
29
|
export default function footerExtension(pi: ExtensionAPI) {
|
|
30
30
|
if (process.env.CC_FOOTER === "0") return;
|
|
@@ -38,6 +38,27 @@ export default function footerExtension(pi: ExtensionAPI) {
|
|
|
38
38
|
let pr: number | undefined;
|
|
39
39
|
/** Bumped per branch change so a slow gh lookup for an old branch is ignored. */
|
|
40
40
|
let prToken = 0;
|
|
41
|
+
/**
|
|
42
|
+
* The PR is looked up again once a minute while the session is in use, so a
|
|
43
|
+
* PR merged or closed mid-session disappears (pr.ts nextPrPollDelay). A slow
|
|
44
|
+
* or missing `gh` turns polling off; branch changes still look up.
|
|
45
|
+
*/
|
|
46
|
+
const prPoll = { lastLookupAt: 0, lastInputAt: Date.now(), disabled: false };
|
|
47
|
+
let prPollTimer: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
const stopPrPoll = () => {
|
|
49
|
+
clearTimeout(prPollTimer);
|
|
50
|
+
prPollTimer = undefined;
|
|
51
|
+
};
|
|
52
|
+
const schedulePrPoll = () => {
|
|
53
|
+
stopPrPoll();
|
|
54
|
+
const delay = nextPrPollDelay(prPoll, Date.now());
|
|
55
|
+
if (delay === undefined) return;
|
|
56
|
+
prPollTimer = setTimeout(() => {
|
|
57
|
+
prPollTimer = undefined;
|
|
58
|
+
refreshLocation();
|
|
59
|
+
}, delay);
|
|
60
|
+
prPollTimer.unref?.();
|
|
61
|
+
};
|
|
41
62
|
|
|
42
63
|
/** Invalidate the memoized line and repaint; set once the footer mounts. */
|
|
43
64
|
let repaint = () => {};
|
|
@@ -71,20 +92,43 @@ export default function footerExtension(pi: ExtensionAPI) {
|
|
|
71
92
|
|
|
72
93
|
const refreshPr = (cwd: string, branch: string | null) => {
|
|
73
94
|
const token = ++prToken;
|
|
95
|
+
stopPrPoll();
|
|
74
96
|
if (!branch) {
|
|
75
97
|
pr = undefined;
|
|
76
98
|
repaint();
|
|
77
99
|
return;
|
|
78
100
|
}
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
prPoll.lastLookupAt = startedAt;
|
|
79
103
|
void fetchPrNumber(cwd, branch).then((found) => {
|
|
80
104
|
if (token !== prToken) return; // a newer branch superseded this lookup
|
|
81
|
-
|
|
105
|
+
if (found.ghMissing || Date.now() - startedAt > PR_SLOW_LOOKUP_MS) prPoll.disabled = true;
|
|
106
|
+
pr = found.pr;
|
|
82
107
|
repaint();
|
|
108
|
+
schedulePrPoll();
|
|
83
109
|
});
|
|
84
110
|
};
|
|
85
111
|
|
|
112
|
+
// Input keeps polling alive; after an hour without any, the next input
|
|
113
|
+
// resumes it. A turn boundary asks again, never sooner than a minute after
|
|
114
|
+
// the last lookup.
|
|
115
|
+
pi.on("input", () => {
|
|
116
|
+
prPoll.lastInputAt = Date.now();
|
|
117
|
+
if (!prPollTimer) schedulePrPoll();
|
|
118
|
+
});
|
|
119
|
+
pi.on("agent_settled", () => {
|
|
120
|
+
if (!prPollTimer) schedulePrPoll();
|
|
121
|
+
});
|
|
122
|
+
pi.on("session_shutdown", () => {
|
|
123
|
+
stopPrPoll();
|
|
124
|
+
prToken++;
|
|
125
|
+
});
|
|
126
|
+
|
|
86
127
|
pi.on("session_start", (_event, ctx) => {
|
|
87
128
|
recomputeMain(ctx);
|
|
129
|
+
// A slow or missing gh turns polling off for one session, not the process.
|
|
130
|
+
prPoll.disabled = false;
|
|
131
|
+
prPoll.lastInputAt = Date.now();
|
|
88
132
|
if (!ctx.hasUI) return;
|
|
89
133
|
|
|
90
134
|
ctx.ui.setFooter((tui: unknown, theme: unknown, footerData: unknown) => {
|
|
@@ -144,6 +188,7 @@ export default function footerExtension(pi: ExtensionAPI) {
|
|
|
144
188
|
return Object.assign(component, {
|
|
145
189
|
dispose: () => {
|
|
146
190
|
stopFollowingBranch();
|
|
191
|
+
stopPrPoll();
|
|
147
192
|
repaint = () => {};
|
|
148
193
|
refreshLocation = () => {};
|
|
149
194
|
},
|