one-code-extension 0.4.1 → 0.6.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 +16 -5
- 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 +40 -28
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +26 -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 +58 -31
- 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 +124 -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 +642 -0
- package/extensions/background/index.ts +614 -248
- package/extensions/background/loop-fire.ts +173 -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 +56 -0
- package/extensions/background/wakeup.ts +178 -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 +66 -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 +250 -78
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +436 -63
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +219 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +35 -15
- package/extensions/context-management/index.ts +36 -28
- 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 +141 -25
- 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 +420 -42
- package/extensions/file-tracker/replay.ts +137 -24
- package/extensions/file-tracker/restore.ts +66 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +45 -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 +209 -58
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/prompt-context.ts +59 -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 +26 -12
- 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/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +146 -0
- package/extensions/lib/agent-loader.ts +18 -3
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-auth.ts +98 -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 +52 -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 +97 -0
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +359 -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/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +71 -19
- 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 +178 -0
- package/extensions/lib/model-catalog/models-dev.json +18533 -0
- package/extensions/lib/model-catalog/openrouter.json +5050 -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 +357 -73
- 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 +38 -2
- package/extensions/lib/plan-mode-channels.ts +42 -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/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +132 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +370 -358
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +547 -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 +24 -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 +112 -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 +129 -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 +159 -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 +76 -12
- 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 +90 -43
- 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 +220 -121
- 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 +164 -73
- 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/model-default/index.ts +86 -19
- 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 +1426 -398
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +11 -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 +276 -92
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/run-state.ts +26 -0
- 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 +64 -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 +14 -7
- package/extensions/recap/index.ts +73 -71
- 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/announce.ts +30 -0
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +469 -132
- package/extensions/skill/invoke.ts +118 -75
- 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 +146 -31
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +82 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1594 -852
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +5 -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/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +135 -80
- package/extensions/subagents/runs.ts +44 -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 +165 -47
- package/extensions/system-prompt/template.ts +34 -51
- 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 +204 -70
- 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/index.ts +366 -144
- 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 +108 -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 +157 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +33 -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 +162 -44
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +21 -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
- package/extensions/tool-search/announce.ts +0 -42
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* preamble, then every report line indented two spaces (a frame-like line at
|
|
13
13
|
* column zero inside the report is thereby forged). Its completion is a
|
|
14
14
|
* separate kind=agent `<task-notification>` whose `<result>` only points at
|
|
15
|
-
* that message — CC's two-message split (docs/decisions/subagents-workflows.md).
|
|
15
|
+
* that message — CC's two-message split (working-docs/decisions/subagents-workflows.md).
|
|
16
16
|
*
|
|
17
17
|
* Wakeups are neither: CC re-invokes the session with the scheduled prompt
|
|
18
18
|
* verbatim (findings §21), so `background/wakeup.ts` sends the prompt as-is.
|
|
@@ -25,13 +25,18 @@
|
|
|
25
25
|
* applies it at dispatch (`frameForDelivery`), never the producers.
|
|
26
26
|
*
|
|
27
27
|
* Every literal below was read out of real CC transcripts and the shipped
|
|
28
|
-
* 2.1.278 binary (docs/findings/24-claude-code-notifications.md) and is locked
|
|
28
|
+
* 2.1.278 binary (working-docs/findings/24-claude-code-notifications.md) and is locked
|
|
29
29
|
* by test/unit/notification-fidelity.test.ts. The delivery engine
|
|
30
30
|
* (`createTaskNotifier`, further down) is unchanged by the frame shape.
|
|
31
31
|
*/
|
|
32
|
-
import
|
|
33
|
-
import {
|
|
32
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
33
|
+
import { randomUUID } from "node:crypto";
|
|
34
|
+
import { type ExtensionAPI, type ExtensionContext, VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent";
|
|
35
|
+
import { withKeepAlive } from "../lsp/keep-alive.ts";
|
|
36
|
+
import { ONE_SHOT_COMMAND_FAILED_CHANNEL, RunOutcomeLatch } from "./interrupt.ts";
|
|
34
37
|
import { escapeXml } from "./local-command.ts";
|
|
38
|
+
import { piFiresBeforeAgentStartForCustomRuns } from "./pi-version.ts";
|
|
39
|
+
import { isUserRun, PROMPT_OPTIONS_CHANNEL } from "./prompt-options.ts";
|
|
35
40
|
import { wrapReminder } from "./reminders.ts";
|
|
36
41
|
|
|
37
42
|
/** pi's session-mode union, derived from the context (the package root does not export ExtensionMode itself). */
|
|
@@ -154,7 +159,7 @@ export type HandBackVerdict = { kind: "blocked"; reason: string } | { kind: "una
|
|
|
154
159
|
* provider body, a stack), and this is the choke point where it enters the
|
|
155
160
|
* model-facing warning, so it must not go in unbounded. The clip is a bound on
|
|
156
161
|
* a diagnostic clause, not on output the model works from, so it is not
|
|
157
|
-
* persisted (docs/decisions/subagents-workflows.md, 2026-09-21); the
|
|
162
|
+
* persisted (working-docs/decisions/subagents-workflows.md, 2026-09-21); the
|
|
158
163
|
* `unavailable` variant marks a clipped reason with an ellipsis, the `blocked`
|
|
159
164
|
* one stays byte-exact with CC.
|
|
160
165
|
*/
|
|
@@ -278,7 +283,7 @@ export function monitorEndedSummary(description: string, status: TaskStatus, pro
|
|
|
278
283
|
* `Workflow "NAME" finished` — UNVERIFIED: the 2.1.278 binary holds no local
|
|
279
284
|
* workflow completion summary (only remote/error variants) and no transcript
|
|
280
285
|
* on this machine carries one, so this mirrors the agent shape
|
|
281
|
-
* (docs/features/notifications/plan.md, Deviations).
|
|
286
|
+
* (working-docs/features/notifications/plan.md, Deviations).
|
|
282
287
|
*/
|
|
283
288
|
export function workflowSummary(name: string, status: TaskStatus, reason?: string): string {
|
|
284
289
|
const suffix = reason ? `: ${reason}` : "";
|
|
@@ -362,6 +367,8 @@ const TAG_RE = {
|
|
|
362
367
|
summary: /<summary>([\s\S]*?)<\/summary>/,
|
|
363
368
|
result: /<result>([\s\S]*?)<\/result>/,
|
|
364
369
|
event: /<event>([\s\S]*?)<\/event>/,
|
|
370
|
+
status: /<status>([\s\S]*?)<\/status>/,
|
|
371
|
+
durationMs: /<duration_ms>(\d+)<\/duration_ms>/,
|
|
365
372
|
};
|
|
366
373
|
|
|
367
374
|
function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
|
|
@@ -371,7 +378,16 @@ function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
|
|
|
371
378
|
/** One frame of a notification's wire text, for display. */
|
|
372
379
|
export type NotificationFrame =
|
|
373
380
|
| { kind: "agent-message"; from: string; body: string; handBack: boolean }
|
|
374
|
-
| {
|
|
381
|
+
| {
|
|
382
|
+
kind: "task";
|
|
383
|
+
summary: string;
|
|
384
|
+
body: string;
|
|
385
|
+
pointer: boolean;
|
|
386
|
+
/** `<status>`, when the frame has one (an agent or shell completion). */
|
|
387
|
+
status?: string;
|
|
388
|
+
/** `<usage>`'s `<duration_ms>`: an agent frame's run time. */
|
|
389
|
+
durationMs?: number;
|
|
390
|
+
}
|
|
375
391
|
| { kind: "text"; text: string };
|
|
376
392
|
|
|
377
393
|
/**
|
|
@@ -393,11 +409,14 @@ export function parseNotificationFrames(wire: string): NotificationFrame[] {
|
|
|
393
409
|
const block = match[1];
|
|
394
410
|
const body = tagBody(block, "result") ?? tagBody(block, "event");
|
|
395
411
|
const result = body === undefined ? "" : unescapeTaskText(body).trim();
|
|
412
|
+
const durationMs = tagBody(block, "durationMs");
|
|
396
413
|
frames.push({
|
|
397
414
|
kind: "task",
|
|
398
415
|
summary: unescapeTaskText(tagBody(block, "summary") ?? ""),
|
|
399
416
|
body: result,
|
|
400
417
|
pointer: result.startsWith(HAND_BACK_POINTER_PREFIX),
|
|
418
|
+
status: tagBody(block, "status"),
|
|
419
|
+
durationMs: durationMs === undefined ? undefined : Number(durationMs),
|
|
401
420
|
});
|
|
402
421
|
} else {
|
|
403
422
|
frames.push({ kind: "agent-message", from: match[2], body: match[3], handBack: match[3].includes(HAND_BACK_PREAMBLE) });
|
|
@@ -464,7 +483,16 @@ export const NOTIFICATION_ID_KEY = "notificationId";
|
|
|
464
483
|
export const NOTIFICATION_BATCH_KEY = "notifications";
|
|
465
484
|
|
|
466
485
|
/** Inject a harness notification (a task/agent completion, a monitor event, a review note) into the conversation. */
|
|
467
|
-
export
|
|
486
|
+
export interface TaskNotifier {
|
|
487
|
+
(customType: string, text: string, details?: Record<string, unknown>): void;
|
|
488
|
+
/**
|
|
489
|
+
* True while notifications are held after an interrupted turn (see
|
|
490
|
+
* `createTaskNotifier`). A producer that emits a stream of batches (a
|
|
491
|
+
* monitor) keeps accumulating its own bounded batch while this is true,
|
|
492
|
+
* instead of adding one held message per flush.
|
|
493
|
+
*/
|
|
494
|
+
holding(): boolean;
|
|
495
|
+
}
|
|
468
496
|
|
|
469
497
|
/**
|
|
470
498
|
* `task_output` announces here that it returned a FINISHED task's output in a
|
|
@@ -501,16 +529,35 @@ export interface TaskNotifierOptions {
|
|
|
501
529
|
* report, so its hand-back would only repeat it.
|
|
502
530
|
*/
|
|
503
531
|
withdrawOnDelivery?: boolean;
|
|
532
|
+
/**
|
|
533
|
+
* Whether pi fires `before_agent_start` for a run `sendMessage` opens from
|
|
534
|
+
* idle (pi 1.2.0 and later). Then such a run builds One Code's system
|
|
535
|
+
* prompt like a typed one, and no notification needs the user-message
|
|
536
|
+
* path (see "First turn of a session"). Defaults to the running pi's
|
|
537
|
+
* version; tests set it to model an older pi.
|
|
538
|
+
*/
|
|
539
|
+
customRunsFireBeforeAgentStart?: boolean;
|
|
540
|
+
/**
|
|
541
|
+
* Runs once a notification this notifier sent reaches the conversation,
|
|
542
|
+
* with its customType and the details it was sent with: at its
|
|
543
|
+
* `message_end`, or at the send for one delivered as a user message (see
|
|
544
|
+
* "First turn of a session"), whose message carries no details for pi to
|
|
545
|
+
* hand back. A producer whose message makes something true for the model
|
|
546
|
+
* (a `/loop` fire's instructions) records that here, not when it calls the
|
|
547
|
+
* notifier: the notifier may still hold the message, and a `/reload`
|
|
548
|
+
* drops it.
|
|
549
|
+
*/
|
|
550
|
+
onDelivered?: (customType: string, details: Record<string, unknown>) => void;
|
|
504
551
|
}
|
|
505
552
|
|
|
506
553
|
/**
|
|
507
554
|
* Custom types that are a turn's INPUT rather than a harness frame (a fired
|
|
508
|
-
* wakeup or
|
|
555
|
+
* wakeup or cron job delivers the model's prompt verbatim). Such a message is
|
|
509
556
|
* never merged with frames — it goes out on its own, in arrival order — or the
|
|
510
557
|
* prompt would abut a `<task-notification>` with nothing marking the switch,
|
|
511
558
|
* and the wrong renderer would draw the pair.
|
|
512
559
|
*/
|
|
513
|
-
export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "
|
|
560
|
+
export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "cron"]);
|
|
514
561
|
|
|
515
562
|
/** Default coalescing window (ms). */
|
|
516
563
|
export const DEFAULT_COALESCE_MS = 250;
|
|
@@ -556,11 +603,27 @@ export const DEFAULT_COALESCE_MS = 250;
|
|
|
556
603
|
* `triggerTurn` restarted, within milliseconds, the very work the user had just
|
|
557
604
|
* interrupted (review M2). So once a turn has settled `aborted` or `error`
|
|
558
605
|
* (a turn that died on a provider error should not be restarted by a
|
|
559
|
-
* notification either), every notification — the re-sends and
|
|
560
|
-
*
|
|
561
|
-
* and
|
|
562
|
-
*
|
|
563
|
-
*
|
|
606
|
+
* notification either), every notification — the re-sends and any new one
|
|
607
|
+
* arriving while idle — is held by the notifier itself, not started as a turn.
|
|
608
|
+
* The hold is bounded and ends with the next turn, however it starts:
|
|
609
|
+
*
|
|
610
|
+
* - The user's next prompt (`before_agent_start`) takes everything held as
|
|
611
|
+
* `nextTurn` messages, which pi appends to that prompt
|
|
612
|
+
* (`_pendingNextTurnMessages`, filled synchronously before pi reads it),
|
|
613
|
+
* exactly Claude Code's behaviour after an interrupt. The held frames merge
|
|
614
|
+
* into one message, as a coalescing window would merge them, so a long
|
|
615
|
+
* absence costs one message, not one per arrival.
|
|
616
|
+
* - Any other turn start (`agent_start`: a typed skill such as `/loop`, which
|
|
617
|
+
* pi runs through `sendMessage` and never through `prompt()`, or a scheduled
|
|
618
|
+
* fire) ends the hold too, and what was held is steered into that turn. A
|
|
619
|
+
* turn that then settles cleanly leaves no hold behind; before, only a typed
|
|
620
|
+
* prompt ended it, so one Esc early in a session silenced every later
|
|
621
|
+
* `/loop` tick.
|
|
622
|
+
* - A fresh re-invocation (a cron or wakeup fire, `REINVOCATION_TYPES`) is
|
|
623
|
+
* never held: it is the turn's input, scheduled by the user or the loop, so
|
|
624
|
+
* it opens its turn as it would have without the interrupt. Only a re-send
|
|
625
|
+
* of one Esc discarded waits. Producers that stream (a monitor) ask
|
|
626
|
+
* `holding()` and keep their own bounded batch meanwhile.
|
|
564
627
|
*
|
|
565
628
|
* Dead sessions: a producer's callback can outlive the session it belongs to
|
|
566
629
|
* (a background shell finishing after `/clear` replaced the session, or after
|
|
@@ -571,16 +634,33 @@ export const DEFAULT_COALESCE_MS = 250;
|
|
|
571
634
|
* anything dispatched afterwards; a throwing `sendMessage` is swallowed as a
|
|
572
635
|
* second line.
|
|
573
636
|
*
|
|
574
|
-
* First turn of a session: pi's idle
|
|
575
|
-
* calls `_runAgentPrompt` directly and
|
|
576
|
-
* `before_agent_start` fires — and our
|
|
637
|
+
* First turn of a session, on pi before 1.2.0: pi's idle
|
|
638
|
+
* `sendMessage(…, {triggerTurn: true})` calls `_runAgentPrompt` directly and
|
|
639
|
+
* skips the `prompt()` preamble, so no `before_agent_start` fires — and our
|
|
640
|
+
* system prompt is set from that hook.
|
|
577
641
|
* A session whose first input was `/loop …` therefore ran its first request on
|
|
578
642
|
* pi's stock system prompt with no reminder stack (review H1, measured). Until
|
|
579
643
|
* no `prompt()` has run in this process, an idle notification is delivered as a
|
|
580
644
|
* user message (`sendUserMessage`, source "extension"), which takes the full
|
|
581
|
-
* prompt path; the cost is that this one notification renders as a user bubble
|
|
582
|
-
*
|
|
583
|
-
*
|
|
645
|
+
* prompt path; the cost is that this one notification renders as a user bubble
|
|
646
|
+
* (the background extension's markdown transformer shows only its headline).
|
|
647
|
+
* The usual first-turn case avoids it: the command that started the work (the
|
|
648
|
+
* `/btw` fork) announces pi's prompt options (lib/prompt-options.ts), which
|
|
649
|
+
* system-prompt builds from, and the notification goes the custom-message way
|
|
650
|
+
* with Claude Code's one-line render. Every later notification goes that way. Its turn skips
|
|
651
|
+
* `before_agent_start` too, and pi drops a run's prompt when the run settles,
|
|
652
|
+
* so the system-prompt extension installs the prompt for it from
|
|
653
|
+
* `context_with_system` (`system-prompt/idle-turn.ts`). Upstream ask:
|
|
654
|
+
* working-docs/upstream_prs.md #17.
|
|
655
|
+
*
|
|
656
|
+
* pi 1.2.0 fires `before_agent_start` for that run too, so there every
|
|
657
|
+
* notification goes the custom-message way from the start. Before, the
|
|
658
|
+
* fallback ran on every pi, and a `/reload` re-runs the factory, which starts
|
|
659
|
+
* over as if no prompt had run: a re-armed `/loop` wakeup went out as a plain
|
|
660
|
+
* user message and lost its customType and no-op fold (lifecycle grid
|
|
661
|
+
* R3-M17). On an older pi the fallback is still right after a reload: the
|
|
662
|
+
* system-prompt extension has lost the options too, and the user-message
|
|
663
|
+
* path is the only one that rebuilds the prompt.
|
|
584
664
|
*/
|
|
585
665
|
export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOptions = {}): TaskNotifier {
|
|
586
666
|
interface Pending {
|
|
@@ -588,6 +668,8 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
588
668
|
text: string;
|
|
589
669
|
details: Record<string, unknown>;
|
|
590
670
|
resent: boolean;
|
|
671
|
+
/** Unmerged arrivals so a held group can withdraw one task without losing its peers. */
|
|
672
|
+
items: Incoming[];
|
|
591
673
|
}
|
|
592
674
|
interface Incoming {
|
|
593
675
|
customType: string;
|
|
@@ -595,16 +677,27 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
595
677
|
details: Record<string, unknown>;
|
|
596
678
|
}
|
|
597
679
|
const coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
|
|
680
|
+
const customRunsPrompt = options.customRunsFireBeforeAgentStart ?? piFiresBeforeAgentStartForCustomRuns(PI_VERSION);
|
|
598
681
|
const pending = new Map<string, Pending>();
|
|
682
|
+
// Every notifier hears every message_end; simultaneous producers must not share an outbox id.
|
|
683
|
+
const producerId = randomUUID();
|
|
599
684
|
let seq = 0;
|
|
685
|
+
const nextId = () => `${producerId}-${++seq}`;
|
|
600
686
|
/** False once the session this notifier belongs to has shut down. */
|
|
601
687
|
let active = true;
|
|
602
|
-
/**
|
|
603
|
-
|
|
688
|
+
/**
|
|
689
|
+
* True once an idle turn can be built without `prompt()`: always on pi
|
|
690
|
+
* 1.2.0 and later; before it, once a prompt has run (the only path that
|
|
691
|
+
* emits before_agent_start there) or a command announced pi's prompt
|
|
692
|
+
* options.
|
|
693
|
+
*/
|
|
694
|
+
let prompted = customRunsPrompt;
|
|
604
695
|
/** True between agent_start and agent_settled. */
|
|
605
696
|
let busy = false;
|
|
606
|
-
/** True from a turn settling aborted/errored until the
|
|
697
|
+
/** True from a turn settling aborted/errored until the next turn starts: hold, do not start turns. */
|
|
607
698
|
let interrupted = false;
|
|
699
|
+
/** What the hold kept, in arrival order; released as one merged message (see the header). */
|
|
700
|
+
let held: Incoming[] = [];
|
|
608
701
|
const outcome = new RunOutcomeLatch();
|
|
609
702
|
/** Notifications waiting for the coalescing window to close. */
|
|
610
703
|
let batch: Incoming[] = [];
|
|
@@ -615,29 +708,24 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
615
708
|
pending.delete(id);
|
|
616
709
|
return;
|
|
617
710
|
}
|
|
711
|
+
// Held after an interrupt (see the header): kept here, out of reach of
|
|
712
|
+
// Esc's queue clearing, so it leaves the outbox now; a re-send at a
|
|
713
|
+
// later settle would duplicate it. A fresh re-invocation is the one
|
|
714
|
+
// exception: it opens its own turn.
|
|
715
|
+
if (interrupted && !busy && (entry.resent || !REINVOCATION_TYPES.has(entry.customType))) {
|
|
716
|
+
pending.delete(id);
|
|
717
|
+
held.push(...entry.items);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
618
720
|
try {
|
|
619
|
-
if (interrupted && !busy) {
|
|
620
|
-
// Held for the user's next prompt (see the header). pi keeps a nextTurn
|
|
621
|
-
// message on the session itself, out of reach of Esc's queue clearing,
|
|
622
|
-
// so it counts as delivered now: a re-send at a later settle would
|
|
623
|
-
// duplicate it.
|
|
624
|
-
pending.delete(id);
|
|
625
|
-
pi.sendMessage(
|
|
626
|
-
{
|
|
627
|
-
customType: entry.customType,
|
|
628
|
-
content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
|
|
629
|
-
display: true,
|
|
630
|
-
details: { ...entry.details, [NOTIFICATION_ID_KEY]: id },
|
|
631
|
-
},
|
|
632
|
-
{ deliverAs: "nextTurn" },
|
|
633
|
-
);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
721
|
if (!prompted && !busy) {
|
|
637
722
|
// No confirmation possible for a user-role message (no details), and
|
|
638
723
|
// prompt() cannot be cleared by Esc before it starts: count it delivered.
|
|
639
724
|
pending.delete(id);
|
|
640
|
-
pi.
|
|
725
|
+
// followUp: pi 1.2.0 counts a starting run as busy and rejects a
|
|
726
|
+
// bare send there; when idle, the option changes nothing.
|
|
727
|
+
pi.sendUserMessage(frameForDelivery(entry.text, "opens-turn"), { deliverAs: "followUp" });
|
|
728
|
+
options.onDelivered?.(entry.customType, entry.details);
|
|
641
729
|
return;
|
|
642
730
|
}
|
|
643
731
|
pi.sendMessage(
|
|
@@ -656,43 +744,56 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
656
744
|
}
|
|
657
745
|
};
|
|
658
746
|
|
|
659
|
-
/** One
|
|
747
|
+
/** One entry for a group: a lone notification as it came, several merged under one text. */
|
|
748
|
+
const merge = (group: Incoming[]): Incoming =>
|
|
749
|
+
group.length === 1
|
|
750
|
+
? group[0]
|
|
751
|
+
: {
|
|
752
|
+
customType: group[0].customType,
|
|
753
|
+
text: mergeNotificationTexts(group.map((n) => n.text)),
|
|
754
|
+
// Keep any pre-batched metadata flat.
|
|
755
|
+
details: {
|
|
756
|
+
[NOTIFICATION_BATCH_KEY]: group.flatMap(
|
|
757
|
+
(n) => (n.details[NOTIFICATION_BATCH_KEY] as unknown[] | undefined) ?? [{ customType: n.customType, details: n.details }],
|
|
758
|
+
),
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
/** One outbox entry per group. */
|
|
660
763
|
const enqueue = (group: Incoming[]) => {
|
|
661
764
|
if (group.length === 0) return;
|
|
662
|
-
const id =
|
|
663
|
-
const entry: Pending =
|
|
664
|
-
group.length === 1
|
|
665
|
-
? { ...group[0], resent: false }
|
|
666
|
-
: {
|
|
667
|
-
customType: group[0].customType,
|
|
668
|
-
text: mergeNotificationTexts(group.map((n) => n.text)),
|
|
669
|
-
details: { [NOTIFICATION_BATCH_KEY]: group.map((n) => ({ customType: n.customType, details: n.details })) },
|
|
670
|
-
resent: false,
|
|
671
|
-
};
|
|
765
|
+
const id = nextId();
|
|
766
|
+
const entry: Pending = { ...merge(group), resent: false, items: group };
|
|
672
767
|
pending.set(id, entry);
|
|
673
768
|
dispatch(id, entry);
|
|
674
769
|
};
|
|
675
770
|
|
|
676
771
|
/**
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
* order is kept across the two.
|
|
772
|
+
* Split arrivals into delivery groups: frames merge, a standalone
|
|
773
|
+
* (turn-input) message stands alone, arrival order is kept across the two.
|
|
680
774
|
*/
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
const incoming = batch;
|
|
684
|
-
batch = [];
|
|
775
|
+
const groups = (incoming: Incoming[]): Incoming[][] => {
|
|
776
|
+
const out: Incoming[][] = [];
|
|
685
777
|
let frames: Incoming[] = [];
|
|
686
778
|
for (const item of incoming) {
|
|
687
779
|
if (!REINVOCATION_TYPES.has(item.customType)) {
|
|
688
780
|
frames.push(item);
|
|
689
781
|
continue;
|
|
690
782
|
}
|
|
691
|
-
|
|
783
|
+
if (frames.length > 0) out.push(frames);
|
|
692
784
|
frames = [];
|
|
693
|
-
|
|
785
|
+
out.push([item]);
|
|
694
786
|
}
|
|
695
|
-
|
|
787
|
+
if (frames.length > 0) out.push(frames);
|
|
788
|
+
return out;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
/** Close the coalescing window. */
|
|
792
|
+
const flush = () => {
|
|
793
|
+
batchTimer = undefined;
|
|
794
|
+
const incoming = batch;
|
|
795
|
+
batch = [];
|
|
796
|
+
for (const group of groups(incoming)) enqueue(group);
|
|
696
797
|
};
|
|
697
798
|
const clearBatch = () => {
|
|
698
799
|
if (batchTimer !== undefined) clearTimeout(batchTimer);
|
|
@@ -700,11 +801,46 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
700
801
|
batch = [];
|
|
701
802
|
};
|
|
702
803
|
|
|
804
|
+
/** End the hold. `withPrompt`: the user's prompt is being built, so ride it as nextTurn; else steer into the turn that started. */
|
|
805
|
+
const release = (withPrompt: boolean) => {
|
|
806
|
+
interrupted = false;
|
|
807
|
+
const released = held;
|
|
808
|
+
held = [];
|
|
809
|
+
if (!active || released.length === 0) return;
|
|
810
|
+
for (const group of groups(released)) {
|
|
811
|
+
if (!withPrompt) {
|
|
812
|
+
enqueue(group);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const entry = merge(group);
|
|
816
|
+
try {
|
|
817
|
+
// Synchronous up to pi's push onto `_pendingNextTurnMessages`, which
|
|
818
|
+
// the prompt being built reads right after this hook; pi keeps it out
|
|
819
|
+
// of reach of Esc's queue clearing, so no outbox entry.
|
|
820
|
+
pi.sendMessage(
|
|
821
|
+
{
|
|
822
|
+
customType: entry.customType,
|
|
823
|
+
content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
|
|
824
|
+
display: true,
|
|
825
|
+
details: { ...entry.details, [NOTIFICATION_ID_KEY]: nextId() },
|
|
826
|
+
},
|
|
827
|
+
{ deliverAs: "nextTurn" },
|
|
828
|
+
);
|
|
829
|
+
} catch {
|
|
830
|
+
// Disposed under us (see dispatch).
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
703
835
|
pi.on("message_end", (event) => {
|
|
704
|
-
const message = event.message as { role?: string; details?: unknown };
|
|
836
|
+
const message = event.message as { role?: string; customType?: string; details?: unknown };
|
|
705
837
|
if (message.role !== "custom") return;
|
|
706
|
-
const
|
|
707
|
-
|
|
838
|
+
const details = message.details as Record<string, unknown> | undefined;
|
|
839
|
+
const id = details?.[NOTIFICATION_ID_KEY];
|
|
840
|
+
if (typeof id !== "string") return;
|
|
841
|
+
pending.delete(id);
|
|
842
|
+
// Every notifier hears every message_end: only its own ids are its deliveries.
|
|
843
|
+
if (details && id.startsWith(`${producerId}-`)) options.onDelivered?.(message.customType ?? "", details);
|
|
708
844
|
});
|
|
709
845
|
pi.on("agent_end", (event, ctx) => {
|
|
710
846
|
outcome.record(event.messages, ctx.signal?.aborted);
|
|
@@ -724,33 +860,54 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
724
860
|
});
|
|
725
861
|
pi.on("agent_start", () => {
|
|
726
862
|
busy = true;
|
|
863
|
+
// A turn the user did not type (a skill such as /loop, a scheduled fire):
|
|
864
|
+
// the session is working again, so what was held steers into it.
|
|
865
|
+
if (interrupted) release(false);
|
|
727
866
|
});
|
|
728
|
-
pi.on("before_agent_start", () => {
|
|
867
|
+
pi.on("before_agent_start", (event) => {
|
|
868
|
+
prompted = true;
|
|
869
|
+
// A run an extension opened (pi 1.2.0 fires this for it too) has no user
|
|
870
|
+
// message to ride; agent_start releases into it as before.
|
|
871
|
+
if (isUserRun(event)) release(true);
|
|
872
|
+
});
|
|
873
|
+
pi.events?.on(PROMPT_OPTIONS_CHANNEL, () => {
|
|
729
874
|
prompted = true;
|
|
730
|
-
interrupted = false;
|
|
731
875
|
});
|
|
732
876
|
// A replaced session (/clear, /new, resume) has no use for the old one's undelivered notices.
|
|
733
877
|
pi.on("session_start", () => {
|
|
734
878
|
pending.clear();
|
|
735
879
|
clearBatch();
|
|
880
|
+
held = [];
|
|
736
881
|
active = true;
|
|
737
|
-
prompted =
|
|
882
|
+
prompted = customRunsPrompt;
|
|
738
883
|
busy = false;
|
|
739
884
|
interrupted = false;
|
|
740
885
|
});
|
|
741
886
|
pi.on("session_shutdown", () => {
|
|
742
887
|
active = false;
|
|
743
888
|
clearBatch();
|
|
889
|
+
held = [];
|
|
744
890
|
});
|
|
745
891
|
if (options.withdrawOnDelivery) {
|
|
746
892
|
pi.events?.on(TASK_OUTPUT_DELIVERED_CHANNEL, (data) => {
|
|
747
893
|
const taskId = (data as TaskOutputDelivered | undefined)?.taskId;
|
|
748
894
|
if (taskId === undefined) return;
|
|
749
|
-
|
|
895
|
+
const keep = (item: Incoming) => item.details.taskId !== taskId;
|
|
896
|
+
batch = batch.filter(keep);
|
|
897
|
+
held = held.filter(keep);
|
|
898
|
+
// A sent entry still awaiting confirmation is resent (or held) when an
|
|
899
|
+
// interrupt discards pi's queue: drop the task from it too, re-merging
|
|
900
|
+
// what remains, so the model is not told of output it already has.
|
|
901
|
+
for (const [id, entry] of [...pending]) {
|
|
902
|
+
const items = entry.items.filter(keep);
|
|
903
|
+
if (items.length === entry.items.length) continue;
|
|
904
|
+
if (items.length === 0) pending.delete(id);
|
|
905
|
+
else Object.assign(entry, merge(items), { items });
|
|
906
|
+
}
|
|
750
907
|
});
|
|
751
908
|
}
|
|
752
909
|
|
|
753
|
-
|
|
910
|
+
const notify = (customType: string, text: string, details: Record<string, unknown> = {}) => {
|
|
754
911
|
if (!active) return;
|
|
755
912
|
batch.push({ customType, text, details });
|
|
756
913
|
if (coalesceMs <= 0) {
|
|
@@ -762,6 +919,7 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
|
|
|
762
919
|
(batchTimer as { unref?: () => void }).unref?.();
|
|
763
920
|
}
|
|
764
921
|
};
|
|
922
|
+
return Object.assign(notify, { holding: () => active && interrupted && !busy });
|
|
765
923
|
}
|
|
766
924
|
|
|
767
925
|
/**
|
|
@@ -786,10 +944,136 @@ export function sessionOutlivesTurn(mode: ExtensionMode): boolean {
|
|
|
786
944
|
}
|
|
787
945
|
}
|
|
788
946
|
|
|
947
|
+
/**
|
|
948
|
+
* What a one-shot session (`-p`, `--mode json`) changes for the model, said
|
|
949
|
+
* before its first call. Without it the tool descriptions promise detached
|
|
950
|
+
* work, notifications and questions the session cannot deliver, and the model
|
|
951
|
+
* learns otherwise only from a result: in the 2026-10-04 Codex battery both
|
|
952
|
+
* GPT-6 models planned around background shells, agents, monitors, cron jobs
|
|
953
|
+
* and the question dialog, then had to retract. One Code's own text; Claude
|
|
954
|
+
* Code sends no such block.
|
|
955
|
+
*/
|
|
956
|
+
export function oneShotSessionNote(): string {
|
|
957
|
+
return [
|
|
958
|
+
"This is a non-interactive one-shot session: it ends when this turn ends, and nobody can reply before then.",
|
|
959
|
+
"- Work you start in the background (a shell command, an agent, a monitor) runs to completion before its call returns, and no notification follows. To overlap independent work, issue those calls together in one batch.",
|
|
960
|
+
"- Scheduled and recurring jobs never fire.",
|
|
961
|
+
"- No user can answer a question or approve a plan. State your assumption and proceed, or ask in your final reply and stop.",
|
|
962
|
+
].join("\n");
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* The notice that a session whose first message said it was one-shot runs
|
|
967
|
+
* interactively now (a `-p` session continued in the TUI). One Code's own text.
|
|
968
|
+
*/
|
|
969
|
+
export function interactiveSessionNote(): string {
|
|
970
|
+
return "This session is interactive now, although its first message said it was one-shot: a user can reply, background work notifies you when it ends, scheduled jobs fire, and questions and plan approvals reach the user.";
|
|
971
|
+
}
|
|
972
|
+
|
|
789
973
|
function assertNever(mode: never): never {
|
|
790
974
|
throw new Error(`Unhandled session mode: ${String(mode)}`);
|
|
791
975
|
}
|
|
792
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Submit a user turn and keep a one-shot command alive through preflight AND
|
|
979
|
+
* settlement. Create once at extension factory scope, not once per command.
|
|
980
|
+
*
|
|
981
|
+
* Unlike sendMessage, sendUserMessage awaits input hooks/auth/before_agent_start
|
|
982
|
+
* before marking the run active. waitForIdle alone therefore returns too soon.
|
|
983
|
+
* Serialize one-shot commands across extensions and wait out any active run
|
|
984
|
+
* before sending: a queued message need not emit agent_start. Correlate that
|
|
985
|
+
* event with the sending async context, never the next unrelated start. Then
|
|
986
|
+
* waitForIdle covers tools, follow-ups and retries. TUI/RPC stay fire-and-forget.
|
|
987
|
+
*
|
|
988
|
+
* pi's void API reports preflight errors separately, without a completion event.
|
|
989
|
+
* Bound the start wait so an error or an input hook consuming the prompt cannot
|
|
990
|
+
* hang a print process forever. The timeout applies only to startup, not the run.
|
|
991
|
+
*/
|
|
992
|
+
export function createUserMessageSender(pi: Pick<ExtensionAPI, "on" | "sendUserMessage" | "events">) {
|
|
993
|
+
type Waiter = { start(): void; stop(): void };
|
|
994
|
+
const pending = new Set<Waiter>();
|
|
995
|
+
const sending = new AsyncLocalStorage<Waiter>();
|
|
996
|
+
let active = true;
|
|
997
|
+
let previous = Promise.resolve();
|
|
998
|
+
// Each extension has its own module instance. Reserve the same position in
|
|
999
|
+
// all senders through the session bus, not shared module state.
|
|
1000
|
+
pi.events.on("one-code:user-turn-queue", (data) => {
|
|
1001
|
+
if (!active) return;
|
|
1002
|
+
const reservation = data as { before: Promise<void>[]; done: Promise<void> };
|
|
1003
|
+
reservation.before.push(previous);
|
|
1004
|
+
previous = reservation.done;
|
|
1005
|
+
});
|
|
1006
|
+
pi.on("agent_start", () => {
|
|
1007
|
+
// Async preflight preserves the sending call's context, including input
|
|
1008
|
+
// transformations. An unrelated turn must not release this command.
|
|
1009
|
+
sending.getStore()?.start();
|
|
1010
|
+
});
|
|
1011
|
+
pi.on("session_shutdown", () => {
|
|
1012
|
+
active = false;
|
|
1013
|
+
for (const waiter of pending) waiter.stop();
|
|
1014
|
+
pending.clear();
|
|
1015
|
+
sending.disable();
|
|
1016
|
+
});
|
|
1017
|
+
// A replacement session (/new, /resume) reuses this extension instance:
|
|
1018
|
+
// its commands report their failures again, and never queue behind a
|
|
1019
|
+
// reservation the old session left unreleased.
|
|
1020
|
+
pi.on("session_start", () => {
|
|
1021
|
+
active = true;
|
|
1022
|
+
previous = Promise.resolve();
|
|
1023
|
+
});
|
|
1024
|
+
return async (
|
|
1025
|
+
ctx: Parameters<typeof awaitOneShotTurn>[0],
|
|
1026
|
+
content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
|
|
1027
|
+
options?: Parameters<ExtensionAPI["sendUserMessage"]>[1],
|
|
1028
|
+
): Promise<void> => {
|
|
1029
|
+
if (!active) throw new Error("Session shut down before the command's user turn completed.");
|
|
1030
|
+
if (sessionOutlivesTurn(ctx.mode)) {
|
|
1031
|
+
// followUp by default: pi 1.2.0 counts a starting run as busy and
|
|
1032
|
+
// rejects a send without deliverAs there; when idle it changes nothing.
|
|
1033
|
+
pi.sendUserMessage(content, { ...options, deliverAs: options?.deliverAs ?? "followUp" });
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
let release!: () => void;
|
|
1037
|
+
const reservation = { before: [] as Promise<void>[], done: new Promise<void>((resolve) => { release = resolve; }) };
|
|
1038
|
+
pi.events.emit("one-code:user-turn-queue", reservation);
|
|
1039
|
+
await withKeepAlive(async () => {
|
|
1040
|
+
let start!: () => void;
|
|
1041
|
+
let fail!: (error: Error) => void;
|
|
1042
|
+
const started = new Promise<void>((resolve) => { start = resolve; });
|
|
1043
|
+
const stopped = new Promise<never>((_resolve, reject) => { fail = reject; });
|
|
1044
|
+
const waiter = { start, stop: () => fail(new Error("Session shut down before the command's user turn completed.")) };
|
|
1045
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1046
|
+
pending.add(waiter);
|
|
1047
|
+
try {
|
|
1048
|
+
await Promise.race([Promise.all(reservation.before), stopped]);
|
|
1049
|
+
// A queued user message can be consumed in an existing run without
|
|
1050
|
+
// another agent_start. One-shot commands run after that run settles.
|
|
1051
|
+
if (!ctx.isIdle()) await Promise.race([awaitOneShotTurn(ctx), stopped]);
|
|
1052
|
+
if (!active) throw new Error("Session shut down before the command's user turn completed.");
|
|
1053
|
+
timer = setTimeout(() => fail(new Error("Command's user turn did not start within 30 seconds. Check model authentication and extension input hooks, then retry.")), 30_000);
|
|
1054
|
+
// A raw extension send can still start during preflight. Without a
|
|
1055
|
+
// per-send queue acknowledgement, reject that race rather than run
|
|
1056
|
+
// a queued command we would then incorrectly report as not started.
|
|
1057
|
+
sending.run(waiter, () => pi.sendUserMessage(content, options ? { ...options, deliverAs: undefined } : undefined));
|
|
1058
|
+
await Promise.race([started, stopped]);
|
|
1059
|
+
clearTimeout(timer);
|
|
1060
|
+
await Promise.race([awaitOneShotTurn(ctx), stopped]);
|
|
1061
|
+
} finally {
|
|
1062
|
+
clearTimeout(timer);
|
|
1063
|
+
pending.delete(waiter);
|
|
1064
|
+
release();
|
|
1065
|
+
}
|
|
1066
|
+
}).catch((error) => {
|
|
1067
|
+
// pi logs command-handler rejections but runPrintMode still returns
|
|
1068
|
+
// success. Tell the session's exit extension without touching the
|
|
1069
|
+
// process-wide code here (a later prompt may succeed, or this may be
|
|
1070
|
+
// a child). A shutdown rejection must not act on a disposed session.
|
|
1071
|
+
if (active) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
|
|
1072
|
+
throw error;
|
|
1073
|
+
});
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
793
1077
|
/**
|
|
794
1078
|
* Block until the current agent run settles, but only in one-shot modes.
|
|
795
1079
|
*
|
|
@@ -803,8 +1087,8 @@ function assertNever(mode: never): never {
|
|
|
803
1087
|
*
|
|
804
1088
|
* `waitForIdle()` exists on a command context; a plain event-handler context
|
|
805
1089
|
* (e.g. the `input` hook) only exposes `isIdle()`, so poll that as the fallback.
|
|
806
|
-
* `pi.sendMessage` marks the run active synchronously
|
|
807
|
-
*
|
|
1090
|
+
* `pi.sendMessage` marks the run active synchronously. `sendUserMessage` does
|
|
1091
|
+
* not: use createUserMessageSender for that API so its preflight finishes first.
|
|
808
1092
|
*/
|
|
809
1093
|
export async function awaitOneShotTurn(ctx: {
|
|
810
1094
|
mode: ExtensionMode;
|