@hyperspaceng/neural-coding-agent 0.62.1 → 0.63.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -0
- package/README.md +10 -0
- package/dist/bun/register-bedrock.d.ts.map +1 -1
- package/dist/bun/register-bedrock.js +2 -2
- package/dist/bun/register-bedrock.js.map +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/file-processor.d.ts +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +4 -0
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/initial-message.d.ts +1 -1
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +1 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +1 -1
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/core/agent-session.d.ts +21 -11
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +159 -101
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +5 -3
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +7 -4
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +4 -2
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +3 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +5 -5
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +28 -27
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +2 -2
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/defaults.d.ts +1 -1
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/export-html/index.d.ts +3 -3
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +7 -6
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts +2 -2
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +41 -16
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +27 -17
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +7 -10
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +27 -38
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +56 -15
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts +1 -1
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +2 -8
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/keybindings.d.ts +2 -2
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +1 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/messages.d.ts +3 -3
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +19 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +85 -71
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -2
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +5 -5
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts +6 -0
- package/dist/core/output-guard.d.ts.map +1 -0
- package/dist/core/output-guard.js +59 -0
- package/dist/core/output-guard.js.map +1 -0
- package/dist/core/package-manager.d.ts +3 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +153 -29
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +2 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +30 -32
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +6 -0
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +37 -5
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -5
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +136 -108
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +15 -24
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +2 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +2 -3
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/source-info.d.ts +18 -0
- package/dist/core/source-info.d.ts.map +1 -0
- package/dist/core/source-info.js +19 -0
- package/dist/core/source-info.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -38
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/timings.d.ts +1 -0
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +6 -0
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/bash.d.ts +20 -10
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +151 -59
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +23 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +150 -57
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +31 -7
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +179 -59
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +4 -4
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/find.d.ts +12 -5
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +76 -27
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +16 -5
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +83 -29
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +68 -22
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +50 -26
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +10 -4
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +67 -13
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +11 -4
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +110 -51
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +21 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -0
- package/dist/core/tools/render-utils.js +49 -0
- package/dist/core/tools/render-utils.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.js +30 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
- package/dist/core/tools/write.d.ts +10 -4
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +162 -27
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +57 -19
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts +1 -1
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +2 -2
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +1 -1
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +1 -2
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +19 -6
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +1 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.d.ts +1 -1
- package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
- package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +1 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +1 -1
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts +1 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +1 -1
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +1 -1
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +1 -1
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +2 -2
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +2 -2
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -2
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +2 -2
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +1 -1
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +1 -1
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -3
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.d.ts +1 -1
- package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.js +1 -1
- package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/theme-selector.d.ts +1 -1
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/theme-selector.js +1 -1
- package/dist/modes/interactive/components/theme-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts +2 -2
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +1 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +15 -41
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +121 -679
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +1 -1
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +1 -1
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +1 -1
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.js +1 -1
- package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +5 -13
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +175 -160
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +5 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +15 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +2 -2
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +84 -78
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +27 -20
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +5 -6
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/image-resize.d.ts +6 -6
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +45 -94
- package/dist/utils/image-resize.js.map +1 -1
- package/docs/development.md +3 -1
- package/docs/extensions.md +85 -34
- package/docs/models.md +6 -0
- package/docs/rpc.md +11 -2
- package/docs/settings.md +12 -0
- package/docs/skills.md +3 -2
- package/docs/tui.md +2 -2
- package/examples/extensions/built-in-tool-renderer.ts +8 -8
- package/examples/extensions/commands.ts +3 -3
- package/examples/extensions/custom-compaction.ts +17 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +17 -17
- package/examples/extensions/custom-provider-gitlab-duo/package.json +14 -14
- package/examples/extensions/custom-provider-qwen-cli/package.json +14 -14
- package/examples/extensions/handoff.ts +5 -2
- package/examples/extensions/minimal-mode.ts +14 -14
- package/examples/extensions/qna.ts +5 -2
- package/examples/extensions/question.ts +2 -2
- package/examples/extensions/questionnaire.ts +2 -2
- package/examples/extensions/subagent/index.ts +23 -3
- package/examples/extensions/summarize.ts +15 -4
- package/examples/extensions/todo.ts +2 -2
- package/examples/extensions/trigger-compact.ts +11 -1
- package/examples/extensions/truncated-tool.ts +2 -2
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +20 -20
- package/examples/sdk/04-skills.ts +8 -2
- package/examples/sdk/08-prompt-templates.ts +2 -1
- package/examples/sdk/12-full-control.ts +0 -1
- package/package.json +5 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyExifOrientation } from "./exif-orientation.js";
|
|
2
2
|
import { loadPhoton } from "./photon.js";
|
|
3
|
-
// 4.5MB
|
|
3
|
+
// 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit.
|
|
4
4
|
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
5
5
|
const DEFAULT_OPTIONS = {
|
|
6
6
|
maxWidth: 2000,
|
|
@@ -8,38 +8,34 @@ const DEFAULT_OPTIONS = {
|
|
|
8
8
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
9
9
|
jpegQuality: 80,
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
11
|
+
function encodeCandidate(buffer, mimeType) {
|
|
12
|
+
const data = Buffer.from(buffer).toString("base64");
|
|
13
|
+
return {
|
|
14
|
+
data,
|
|
15
|
+
encodedSize: Buffer.byteLength(data, "utf-8"),
|
|
16
|
+
mimeType,
|
|
17
|
+
};
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
|
-
* Resize an image to fit within the specified max dimensions and file size.
|
|
17
|
-
* Returns the
|
|
20
|
+
* Resize an image to fit within the specified max dimensions and encoded file size.
|
|
21
|
+
* Returns null if the image cannot be resized below maxBytes.
|
|
18
22
|
*
|
|
19
23
|
* Uses Photon (Rust/WASM) for image processing. If Photon is not available,
|
|
20
|
-
* returns
|
|
24
|
+
* returns null.
|
|
21
25
|
*
|
|
22
26
|
* Strategy for staying under maxBytes:
|
|
23
27
|
* 1. First resize to maxWidth/maxHeight
|
|
24
28
|
* 2. Try both PNG and JPEG formats, pick the smaller one
|
|
25
29
|
* 3. If still too large, try JPEG with decreasing quality
|
|
26
|
-
* 4. If still too large, progressively reduce dimensions
|
|
30
|
+
* 4. If still too large, progressively reduce dimensions until 1x1
|
|
27
31
|
*/
|
|
28
32
|
export async function resizeImage(img, options) {
|
|
29
33
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
30
34
|
const inputBuffer = Buffer.from(img.data, "base64");
|
|
35
|
+
const inputBase64Size = Buffer.byteLength(img.data, "utf-8");
|
|
31
36
|
const photon = await loadPhoton();
|
|
32
37
|
if (!photon) {
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
data: img.data,
|
|
36
|
-
mimeType: img.mimeType,
|
|
37
|
-
originalWidth: 0,
|
|
38
|
-
originalHeight: 0,
|
|
39
|
-
width: 0,
|
|
40
|
-
height: 0,
|
|
41
|
-
wasResized: false,
|
|
42
|
-
};
|
|
38
|
+
return null;
|
|
43
39
|
}
|
|
44
40
|
let image;
|
|
45
41
|
try {
|
|
@@ -51,9 +47,8 @@ export async function resizeImage(img, options) {
|
|
|
51
47
|
const originalWidth = image.get_width();
|
|
52
48
|
const originalHeight = image.get_height();
|
|
53
49
|
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
54
|
-
// Check if already within all limits (dimensions AND size)
|
|
55
|
-
|
|
56
|
-
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
50
|
+
// Check if already within all limits (dimensions AND encoded size)
|
|
51
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && inputBase64Size < opts.maxBytes) {
|
|
57
52
|
return {
|
|
58
53
|
data: img.data,
|
|
59
54
|
mimeType: img.mimeType ?? `image/${format}`,
|
|
@@ -75,96 +70,52 @@ export async function resizeImage(img, options) {
|
|
|
75
70
|
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
76
71
|
targetHeight = opts.maxHeight;
|
|
77
72
|
}
|
|
78
|
-
|
|
79
|
-
function tryBothFormats(width, height, jpegQuality) {
|
|
73
|
+
function tryEncodings(width, height, jpegQualities) {
|
|
80
74
|
const resized = photon.resize(image, width, height, photon.SamplingFilter.Lanczos3);
|
|
81
75
|
try {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
76
|
+
const candidates = [encodeCandidate(resized.get_bytes(), "image/png")];
|
|
77
|
+
for (const quality of jpegQualities) {
|
|
78
|
+
candidates.push(encodeCandidate(resized.get_bytes_jpeg(quality), "image/jpeg"));
|
|
79
|
+
}
|
|
80
|
+
return candidates;
|
|
85
81
|
}
|
|
86
82
|
finally {
|
|
87
83
|
resized.free();
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
98
|
-
if (best.buffer.length <= opts.maxBytes) {
|
|
99
|
-
return {
|
|
100
|
-
data: Buffer.from(best.buffer).toString("base64"),
|
|
101
|
-
mimeType: best.mimeType,
|
|
102
|
-
originalWidth,
|
|
103
|
-
originalHeight,
|
|
104
|
-
width: finalWidth,
|
|
105
|
-
height: finalHeight,
|
|
106
|
-
wasResized: true,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
// Still too large - try JPEG with decreasing quality
|
|
110
|
-
for (const quality of qualitySteps) {
|
|
111
|
-
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
112
|
-
if (best.buffer.length <= opts.maxBytes) {
|
|
113
|
-
return {
|
|
114
|
-
data: Buffer.from(best.buffer).toString("base64"),
|
|
115
|
-
mimeType: best.mimeType,
|
|
116
|
-
originalWidth,
|
|
117
|
-
originalHeight,
|
|
118
|
-
width: finalWidth,
|
|
119
|
-
height: finalHeight,
|
|
120
|
-
wasResized: true,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// Still too large - reduce dimensions progressively
|
|
125
|
-
for (const scale of scaleSteps) {
|
|
126
|
-
finalWidth = Math.round(targetWidth * scale);
|
|
127
|
-
finalHeight = Math.round(targetHeight * scale);
|
|
128
|
-
if (finalWidth < 100 || finalHeight < 100) {
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
for (const quality of qualitySteps) {
|
|
132
|
-
best = tryBothFormats(finalWidth, finalHeight, quality);
|
|
133
|
-
if (best.buffer.length <= opts.maxBytes) {
|
|
86
|
+
const qualitySteps = Array.from(new Set([opts.jpegQuality, 85, 70, 55, 40]));
|
|
87
|
+
let currentWidth = targetWidth;
|
|
88
|
+
let currentHeight = targetHeight;
|
|
89
|
+
while (true) {
|
|
90
|
+
const candidates = tryEncodings(currentWidth, currentHeight, qualitySteps);
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
if (candidate.encodedSize < opts.maxBytes) {
|
|
134
93
|
return {
|
|
135
|
-
data:
|
|
136
|
-
mimeType:
|
|
94
|
+
data: candidate.data,
|
|
95
|
+
mimeType: candidate.mimeType,
|
|
137
96
|
originalWidth,
|
|
138
97
|
originalHeight,
|
|
139
|
-
width:
|
|
140
|
-
height:
|
|
98
|
+
width: currentWidth,
|
|
99
|
+
height: currentHeight,
|
|
141
100
|
wasResized: true,
|
|
142
101
|
};
|
|
143
102
|
}
|
|
144
103
|
}
|
|
104
|
+
if (currentWidth === 1 && currentHeight === 1) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
const nextWidth = currentWidth === 1 ? 1 : Math.max(1, Math.floor(currentWidth * 0.75));
|
|
108
|
+
const nextHeight = currentHeight === 1 ? 1 : Math.max(1, Math.floor(currentHeight * 0.75));
|
|
109
|
+
if (nextWidth === currentWidth && nextHeight === currentHeight) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
currentWidth = nextWidth;
|
|
113
|
+
currentHeight = nextHeight;
|
|
145
114
|
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
data: Buffer.from(best.buffer).toString("base64"),
|
|
149
|
-
mimeType: best.mimeType,
|
|
150
|
-
originalWidth,
|
|
151
|
-
originalHeight,
|
|
152
|
-
width: finalWidth,
|
|
153
|
-
height: finalHeight,
|
|
154
|
-
wasResized: true,
|
|
155
|
-
};
|
|
115
|
+
return null;
|
|
156
116
|
}
|
|
157
117
|
catch {
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
data: img.data,
|
|
161
|
-
mimeType: img.mimeType,
|
|
162
|
-
originalWidth: 0,
|
|
163
|
-
originalHeight: 0,
|
|
164
|
-
width: 0,
|
|
165
|
-
height: 0,
|
|
166
|
-
wasResized: false,
|
|
167
|
-
};
|
|
118
|
+
return null;
|
|
168
119
|
}
|
|
169
120
|
finally {
|
|
170
121
|
if (image) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\nimport { applyExifOrientation } from \"./exif-orientation.js\";\nimport { loadPhoton } from \"./photon.js\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Uint8Array; mimeType: string },\n\tb: { buffer: Uint8Array; mimeType: string },\n): { buffer: Uint8Array; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses Photon (Rust/WASM) for image processing. If Photon is not available,\n * returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBuffer = Buffer.from(img.data, \"base64\");\n\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\t// Photon not available, return original image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\tconst inputBytes = new Uint8Array(inputBuffer);\n\t\tconst rawImage = photon.PhotonImage.new_from_byteslice(inputBytes);\n\t\timage = applyExifOrientation(photon, rawImage, inputBytes);\n\t\tif (image !== rawImage) rawImage.free();\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND size)\n\t\tconst originalSize = inputBuffer.length;\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: img.data,\n\t\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\t// Helper to resize and encode in both formats, returning the smaller one\n\t\tfunction tryBothFormats(\n\t\t\twidth: number,\n\t\t\theight: number,\n\t\t\tjpegQuality: number,\n\t\t): { buffer: Uint8Array; mimeType: string } {\n\t\t\tconst resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst pngBuffer = resized.get_bytes();\n\t\t\t\tconst jpegBuffer = resized.get_bytes_jpeg(jpegQuality);\n\n\t\t\t\treturn pickSmaller(\n\t\t\t\t\t{ buffer: pngBuffer, mimeType: \"image/png\" },\n\t\t\t\t\t{ buffer: jpegBuffer, mimeType: \"image/jpeg\" },\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\t// Try to produce an image under maxBytes\n\t\tconst qualitySteps = [85, 70, 55, 40];\n\t\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\t\tlet best: { buffer: Uint8Array; mimeType: string };\n\t\tlet finalWidth = targetWidth;\n\t\tlet finalHeight = targetHeight;\n\n\t\t// First attempt: resize to target dimensions, try both formats\n\t\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\n\t\t// Still too large - try JPEG with decreasing quality\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Still too large - reduce dimensions progressively\n\t\tfor (const scale of scaleSteps) {\n\t\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tfor (const quality of qualitySteps) {\n\t\t\t\tbest = tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\t\theight: finalHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: return smallest version we produced\n\t\treturn {\n\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t} catch {\n\t\t// Failed to load image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\n\t}\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@hyperspaceng/neural-ai\";\nimport { applyExifOrientation } from \"./exif-orientation.js\";\nimport { loadPhoton } from \"./photon.js\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB of base64 payload (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit.\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\ninterface EncodedCandidate {\n\tdata: string;\n\tencodedSize: number;\n\tmimeType: string;\n}\n\nfunction encodeCandidate(buffer: Uint8Array, mimeType: string): EncodedCandidate {\n\tconst data = Buffer.from(buffer).toString(\"base64\");\n\treturn {\n\t\tdata,\n\t\tencodedSize: Buffer.byteLength(data, \"utf-8\"),\n\t\tmimeType,\n\t};\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and encoded file size.\n * Returns null if the image cannot be resized below maxBytes.\n *\n * Uses Photon (Rust/WASM) for image processing. If Photon is not available,\n * returns null.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions until 1x1\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage | null> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBuffer = Buffer.from(img.data, \"base64\");\n\tconst inputBase64Size = Buffer.byteLength(img.data, \"utf-8\");\n\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\tconst inputBytes = new Uint8Array(inputBuffer);\n\t\tconst rawImage = photon.PhotonImage.new_from_byteslice(inputBytes);\n\t\timage = applyExifOrientation(photon, rawImage, inputBytes);\n\t\tif (image !== rawImage) rawImage.free();\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND encoded size)\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && inputBase64Size < opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: img.data,\n\t\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\tfunction tryEncodings(width: number, height: number, jpegQualities: number[]): EncodedCandidate[] {\n\t\t\tconst resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst candidates: EncodedCandidate[] = [encodeCandidate(resized.get_bytes(), \"image/png\")];\n\t\t\t\tfor (const quality of jpegQualities) {\n\t\t\t\t\tcandidates.push(encodeCandidate(resized.get_bytes_jpeg(quality), \"image/jpeg\"));\n\t\t\t\t}\n\t\t\t\treturn candidates;\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\tconst qualitySteps = Array.from(new Set([opts.jpegQuality, 85, 70, 55, 40]));\n\t\tlet currentWidth = targetWidth;\n\t\tlet currentHeight = targetHeight;\n\n\t\twhile (true) {\n\t\t\tconst candidates = tryEncodings(currentWidth, currentHeight, qualitySteps);\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tif (candidate.encodedSize < opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: candidate.data,\n\t\t\t\t\t\tmimeType: candidate.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: currentWidth,\n\t\t\t\t\t\theight: currentHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentWidth === 1 && currentHeight === 1) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tconst nextWidth = currentWidth === 1 ? 1 : Math.max(1, Math.floor(currentWidth * 0.75));\n\t\t\tconst nextHeight = currentHeight === 1 ? 1 : Math.max(1, Math.floor(currentHeight * 0.75));\n\t\t\tif (nextWidth === currentWidth && nextHeight === currentHeight) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcurrentWidth = nextWidth;\n\t\t\tcurrentHeight = nextHeight;\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\n\t}\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
|
package/docs/development.md
CHANGED
|
@@ -14,9 +14,11 @@ npm run build
|
|
|
14
14
|
Run from source:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
|
|
17
|
+
/path/to/pi-mono/pi-test.sh
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
The script can be run from any directory. Pi keeps the caller's current working directory.
|
|
21
|
+
|
|
20
22
|
## Forking / Rebranding
|
|
21
23
|
|
|
22
24
|
Configure via `package.json`:
|
package/docs/extensions.md
CHANGED
|
@@ -293,10 +293,11 @@ Fired by the `pi` CLI during startup session resolution, before the initial sess
|
|
|
293
293
|
This event is:
|
|
294
294
|
- CLI-only. It is not emitted in SDK mode.
|
|
295
295
|
- Startup-only. It is not emitted for later interactive `/new` or `/resume` actions.
|
|
296
|
-
-
|
|
296
|
+
- Lower priority than `--session-dir` and `sessionDir` in `settings.json`.
|
|
297
297
|
- Special-cased to receive no `ctx` argument.
|
|
298
298
|
|
|
299
299
|
If multiple extensions return `sessionDir`, the last one wins.
|
|
300
|
+
Combined precedence is: `--session-dir` CLI flag, then `sessionDir` in settings, then extension `session_directory` hooks.
|
|
300
301
|
|
|
301
302
|
```typescript
|
|
302
303
|
pi.on("session_directory", async (event) => {
|
|
@@ -564,17 +565,27 @@ Before `tool_call` runs, pi waits for previously emitted Agent events to finish
|
|
|
564
565
|
|
|
565
566
|
In the default parallel tool execution mode, sibling tool calls from the same assistant message are preflighted sequentially, then executed concurrently. `tool_call` is not guaranteed to see sibling tool results from that same assistant message in `ctx.sessionManager`.
|
|
566
567
|
|
|
568
|
+
`event.input` is mutable. Mutate it in place to patch tool arguments before execution.
|
|
569
|
+
|
|
570
|
+
Behavior guarantees:
|
|
571
|
+
- Mutations to `event.input` affect the actual tool execution
|
|
572
|
+
- Later `tool_call` handlers see mutations made by earlier handlers
|
|
573
|
+
- No re-validation is performed after your mutation
|
|
574
|
+
- Return values from `tool_call` only control blocking via `{ block: true, reason?: string }`
|
|
575
|
+
|
|
567
576
|
```typescript
|
|
568
577
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
569
578
|
|
|
570
579
|
pi.on("tool_call", async (event, ctx) => {
|
|
571
580
|
// event.toolName - "bash", "read", "write", "edit", etc.
|
|
572
581
|
// event.toolCallId
|
|
573
|
-
// event.input - tool parameters
|
|
582
|
+
// event.input - tool parameters (mutable)
|
|
574
583
|
|
|
575
584
|
// Built-in tools: no type params needed
|
|
576
585
|
if (isToolCallEventType("bash", event)) {
|
|
577
586
|
// event.input is { command: string; timeout?: number }
|
|
587
|
+
event.input.command = `source ~/.profile\n${event.input.command}`;
|
|
588
|
+
|
|
578
589
|
if (event.input.command.includes("rm -rf")) {
|
|
579
590
|
return { block: true, reason: "Dangerous command" };
|
|
580
591
|
}
|
|
@@ -976,8 +987,8 @@ pi.registerTool({
|
|
|
976
987
|
},
|
|
977
988
|
|
|
978
989
|
// Optional: Custom rendering
|
|
979
|
-
renderCall(args, theme) { ... },
|
|
980
|
-
renderResult(result, options, theme) { ... },
|
|
990
|
+
renderCall(args, theme, context) { ... },
|
|
991
|
+
renderResult(result, options, theme, context) { ... },
|
|
981
992
|
});
|
|
982
993
|
```
|
|
983
994
|
|
|
@@ -1089,6 +1100,8 @@ Labels persist in the session and survive restarts. Use them to mark important p
|
|
|
1089
1100
|
|
|
1090
1101
|
Register a command.
|
|
1091
1102
|
|
|
1103
|
+
If multiple extensions register the same command name, pi keeps them all and assigns numeric invocation suffixes in load order, for example `/review:1` and `/review:2`.
|
|
1104
|
+
|
|
1092
1105
|
```typescript
|
|
1093
1106
|
pi.registerCommand("stats", {
|
|
1094
1107
|
description: "Show session statistics",
|
|
@@ -1126,20 +1139,28 @@ The list matches the RPC `get_commands` ordering: extensions first, then templat
|
|
|
1126
1139
|
```typescript
|
|
1127
1140
|
const commands = pi.getCommands();
|
|
1128
1141
|
const bySource = commands.filter((command) => command.source === "extension");
|
|
1142
|
+
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");
|
|
1129
1143
|
```
|
|
1130
1144
|
|
|
1131
1145
|
Each entry has this shape:
|
|
1132
1146
|
|
|
1133
1147
|
```typescript
|
|
1134
1148
|
{
|
|
1135
|
-
name: string; //
|
|
1149
|
+
name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
|
|
1136
1150
|
description?: string;
|
|
1137
1151
|
source: "extension" | "prompt" | "skill";
|
|
1138
|
-
|
|
1139
|
-
|
|
1152
|
+
sourceInfo: {
|
|
1153
|
+
path: string;
|
|
1154
|
+
source: string;
|
|
1155
|
+
scope: "user" | "project" | "temporary";
|
|
1156
|
+
origin: "package" | "top-level";
|
|
1157
|
+
baseDir?: string;
|
|
1158
|
+
};
|
|
1140
1159
|
}
|
|
1141
1160
|
```
|
|
1142
1161
|
|
|
1162
|
+
Use `sourceInfo` as the canonical provenance field. Do not infer ownership from command names or from ad hoc path parsing.
|
|
1163
|
+
|
|
1143
1164
|
Built-in interactive commands (like `/model` and `/settings`) are not included here. They are handled only in interactive
|
|
1144
1165
|
mode and would not execute if sent via `prompt`.
|
|
1145
1166
|
|
|
@@ -1191,12 +1212,27 @@ const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
|
|
|
1191
1212
|
Manage active tools. This works for both built-in tools and dynamically registered tools.
|
|
1192
1213
|
|
|
1193
1214
|
```typescript
|
|
1194
|
-
const active = pi.getActiveTools();
|
|
1195
|
-
const all = pi.getAllTools();
|
|
1196
|
-
|
|
1215
|
+
const active = pi.getActiveTools();
|
|
1216
|
+
const all = pi.getAllTools();
|
|
1217
|
+
// [{
|
|
1218
|
+
// name: "read",
|
|
1219
|
+
// description: "Read file contents...",
|
|
1220
|
+
// parameters: ...,
|
|
1221
|
+
// sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
|
|
1222
|
+
// }, ...]
|
|
1223
|
+
const names = all.map(t => t.name);
|
|
1224
|
+
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
|
|
1225
|
+
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
|
|
1197
1226
|
pi.setActiveTools(["read", "bash"]); // Switch to read-only
|
|
1198
1227
|
```
|
|
1199
1228
|
|
|
1229
|
+
`pi.getAllTools()` returns `name`, `description`, `parameters`, and `sourceInfo`.
|
|
1230
|
+
|
|
1231
|
+
Typical `sourceInfo.source` values:
|
|
1232
|
+
- `builtin` for built-in tools
|
|
1233
|
+
- `sdk` for tools passed via `createAgentSession({ customTools })`
|
|
1234
|
+
- extension source metadata for tools registered by extensions
|
|
1235
|
+
|
|
1200
1236
|
### pi.setModel(model)
|
|
1201
1237
|
|
|
1202
1238
|
Set the current model. Returns `false` if no API key is available for the model. See [models.md](models.md) for configuring custom models.
|
|
@@ -1427,8 +1463,8 @@ pi.registerTool({
|
|
|
1427
1463
|
},
|
|
1428
1464
|
|
|
1429
1465
|
// Optional: Custom rendering
|
|
1430
|
-
renderCall(args, theme) { ... },
|
|
1431
|
-
renderResult(result, options, theme) { ... },
|
|
1466
|
+
renderCall(args, theme, context) { ... },
|
|
1467
|
+
renderResult(result, options, theme, context) { ... },
|
|
1432
1468
|
});
|
|
1433
1469
|
```
|
|
1434
1470
|
|
|
@@ -1463,7 +1499,9 @@ pi --no-tools -e ./my-extension.ts
|
|
|
1463
1499
|
|
|
1464
1500
|
See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) for a complete example that overrides `read` with logging and access control.
|
|
1465
1501
|
|
|
1466
|
-
**Rendering:** If your override
|
|
1502
|
+
**Rendering:** Built-in renderer inheritance is resolved per slot. Execution override and rendering override are independent. If your override omits `renderCall`, the built-in `renderCall` is used. If your override omits `renderResult`, the built-in `renderResult` is used. If your override omits both, the built-in renderer is used automatically (syntax highlighting, diffs, etc.). This lets you wrap built-in tools for logging or access control without reimplementing the UI.
|
|
1503
|
+
|
|
1504
|
+
**Prompt metadata:** `promptSnippet` and `promptGuidelines` are not inherited from the built-in tool. If your override should keep those prompt instructions, define them on the override explicitly.
|
|
1467
1505
|
|
|
1468
1506
|
**Your implementation must match the exact result shape**, including the `details` type. The UI and session logic depend on these shapes for rendering and state tracking.
|
|
1469
1507
|
|
|
@@ -1597,44 +1635,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
1597
1635
|
|
|
1598
1636
|
### Custom Rendering
|
|
1599
1637
|
|
|
1600
|
-
Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) for how
|
|
1638
|
+
Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) for how tool rows are composed.
|
|
1639
|
+
|
|
1640
|
+
Tool output is wrapped in a `Box` that handles padding and background. A defined `renderCall` or `renderResult` must return a `Component`. If a slot renderer is not defined, `tool-execution.ts` uses fallback rendering for that slot.
|
|
1601
1641
|
|
|
1602
|
-
|
|
1642
|
+
`renderCall` and `renderResult` each receive a `context` object with:
|
|
1643
|
+
- `args` - the current tool call arguments
|
|
1644
|
+
- `state` - shared row-local state across `renderCall` and `renderResult`
|
|
1645
|
+
- `lastComponent` - the previously returned component for that slot, if any
|
|
1646
|
+
- `invalidate()` - request a rerender of this tool row
|
|
1647
|
+
- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`
|
|
1648
|
+
|
|
1649
|
+
Use `context.state` for cross-slot shared state. Keep slot-local caches on the returned component instance when you want to reuse and mutate the same component across renders.
|
|
1603
1650
|
|
|
1604
1651
|
#### renderCall
|
|
1605
1652
|
|
|
1606
|
-
Renders the tool call
|
|
1653
|
+
Renders the tool call or header:
|
|
1607
1654
|
|
|
1608
1655
|
```typescript
|
|
1609
1656
|
import { Text } from "@mariozechner/pi-tui";
|
|
1610
1657
|
|
|
1611
|
-
renderCall(args, theme) {
|
|
1612
|
-
|
|
1613
|
-
|
|
1658
|
+
renderCall(args, theme, context) {
|
|
1659
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
1660
|
+
let content = theme.fg("toolTitle", theme.bold("my_tool "));
|
|
1661
|
+
content += theme.fg("muted", args.action);
|
|
1614
1662
|
if (args.text) {
|
|
1615
|
-
|
|
1663
|
+
content += " " + theme.fg("dim", `"${args.text}"`);
|
|
1616
1664
|
}
|
|
1617
|
-
|
|
1665
|
+
text.setText(content);
|
|
1666
|
+
return text;
|
|
1618
1667
|
}
|
|
1619
1668
|
```
|
|
1620
1669
|
|
|
1621
1670
|
#### renderResult
|
|
1622
1671
|
|
|
1623
|
-
Renders the tool result:
|
|
1672
|
+
Renders the tool result or output:
|
|
1624
1673
|
|
|
1625
1674
|
```typescript
|
|
1626
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1627
|
-
// Handle streaming
|
|
1675
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1628
1676
|
if (isPartial) {
|
|
1629
1677
|
return new Text(theme.fg("warning", "Processing..."), 0, 0);
|
|
1630
1678
|
}
|
|
1631
1679
|
|
|
1632
|
-
// Handle errors
|
|
1633
1680
|
if (result.details?.error) {
|
|
1634
1681
|
return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
|
|
1635
1682
|
}
|
|
1636
1683
|
|
|
1637
|
-
// Normal result - support expanded view (Ctrl+O)
|
|
1638
1684
|
let text = theme.fg("success", "✓ Done");
|
|
1639
1685
|
if (expanded && result.details?.items) {
|
|
1640
1686
|
for (const item of result.details.items) {
|
|
@@ -1645,6 +1691,8 @@ renderResult(result, { expanded, isPartial }, theme) {
|
|
|
1645
1691
|
}
|
|
1646
1692
|
```
|
|
1647
1693
|
|
|
1694
|
+
If a slot intentionally has no visible content, return an empty `Component` such as an empty `Container`.
|
|
1695
|
+
|
|
1648
1696
|
#### Keybinding Hints
|
|
1649
1697
|
|
|
1650
1698
|
Use `keyHint()` to display keybinding hints that respect the active keybinding configuration:
|
|
@@ -1652,7 +1700,7 @@ Use `keyHint()` to display keybinding hints that respect the active keybinding c
|
|
|
1652
1700
|
```typescript
|
|
1653
1701
|
import { keyHint } from "@mariozechner/pi-coding-agent";
|
|
1654
1702
|
|
|
1655
|
-
renderResult(result, { expanded }, theme) {
|
|
1703
|
+
renderResult(result, { expanded }, theme, context) {
|
|
1656
1704
|
let text = theme.fg("success", "✓ Done");
|
|
1657
1705
|
if (!expanded) {
|
|
1658
1706
|
text += ` (${keyHint("app.tools.expand", "to expand")})`;
|
|
@@ -1676,16 +1724,19 @@ Custom editors and `ctx.ui.custom()` components receive `keybindings: Keybinding
|
|
|
1676
1724
|
|
|
1677
1725
|
#### Best Practices
|
|
1678
1726
|
|
|
1679
|
-
- Use `Text` with padding `(0, 0)
|
|
1680
|
-
- Use `\n` for multi-line content
|
|
1681
|
-
- Handle `isPartial` for streaming progress
|
|
1682
|
-
- Support `expanded` for detail on demand
|
|
1683
|
-
- Keep default view compact
|
|
1727
|
+
- Use `Text` with padding `(0, 0)`. The Box handles padding.
|
|
1728
|
+
- Use `\n` for multi-line content.
|
|
1729
|
+
- Handle `isPartial` for streaming progress.
|
|
1730
|
+
- Support `expanded` for detail on demand.
|
|
1731
|
+
- Keep default view compact.
|
|
1732
|
+
- Read `context.args` in `renderResult` instead of copying args into `context.state`.
|
|
1733
|
+
- Use `context.state` only for data that must be shared across call and result slots.
|
|
1734
|
+
- Reuse `context.lastComponent` when the same component instance can be updated in place.
|
|
1684
1735
|
|
|
1685
1736
|
#### Fallback
|
|
1686
1737
|
|
|
1687
|
-
If
|
|
1688
|
-
- `renderCall`: Shows tool name
|
|
1738
|
+
If a slot renderer is not defined or throws:
|
|
1739
|
+
- `renderCall`: Shows the tool name
|
|
1689
1740
|
- `renderResult`: Shows raw text from `content`
|
|
1690
1741
|
|
|
1691
1742
|
## Custom UI
|
package/docs/models.md
CHANGED
|
@@ -131,6 +131,12 @@ The `apiKey` and `headers` fields support three formats:
|
|
|
131
131
|
"apiKey": "sk-..."
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
For `models.json`, shell commands are resolved at request time. pi intentionally does not apply built-in TTL, stale reuse, or recovery logic for arbitrary commands. Different commands need different caching and failure strategies, and pi cannot infer the right one.
|
|
135
|
+
|
|
136
|
+
If your command is slow, expensive, rate-limited, or should keep using a previous value on transient failures, wrap it in your own script or command that implements the caching or TTL behavior you want.
|
|
137
|
+
|
|
138
|
+
`/model` availability checks use configured auth presence and do not execute shell commands.
|
|
139
|
+
|
|
134
140
|
### Custom Headers
|
|
135
141
|
|
|
136
142
|
```json
|
package/docs/rpc.md
CHANGED
|
@@ -494,7 +494,7 @@ Response:
|
|
|
494
494
|
|
|
495
495
|
#### get_session_stats
|
|
496
496
|
|
|
497
|
-
Get token usage and
|
|
497
|
+
Get token usage, cost statistics, and current context window usage.
|
|
498
498
|
|
|
499
499
|
```json
|
|
500
500
|
{"type": "get_session_stats"}
|
|
@@ -521,11 +521,20 @@ Response:
|
|
|
521
521
|
"cacheWrite": 5000,
|
|
522
522
|
"total": 105000
|
|
523
523
|
},
|
|
524
|
-
"cost": 0.45
|
|
524
|
+
"cost": 0.45,
|
|
525
|
+
"contextUsage": {
|
|
526
|
+
"tokens": 60000,
|
|
527
|
+
"contextWindow": 200000,
|
|
528
|
+
"percent": 30
|
|
529
|
+
}
|
|
525
530
|
}
|
|
526
531
|
}
|
|
527
532
|
```
|
|
528
533
|
|
|
534
|
+
`tokens` contains assistant usage totals for the current session state. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.
|
|
535
|
+
|
|
536
|
+
`contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.
|
|
537
|
+
|
|
529
538
|
#### export_html
|
|
530
539
|
|
|
531
540
|
Export session to an HTML file.
|
package/docs/settings.md
CHANGED
|
@@ -127,6 +127,18 @@ When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's
|
|
|
127
127
|
|
|
128
128
|
`npmCommand` is used for all npm package-manager operations, including `npm root -g`, installs, uninstalls, and `npm install` inside git packages. Use argv-style entries exactly as the process should be launched.
|
|
129
129
|
|
|
130
|
+
### Sessions
|
|
131
|
+
|
|
132
|
+
| Setting | Type | Default | Description |
|
|
133
|
+
|---------|------|---------|-------------|
|
|
134
|
+
| `sessionDir` | string | - | Directory where session files are stored. Accepts absolute or relative paths. |
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{ "sessionDir": ".pi/sessions" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When multiple sources specify a session directory, `--session-dir` CLI flag takes precedence, then `sessionDir` in settings.json, then extension hooks.
|
|
141
|
+
|
|
130
142
|
### Model Cycling
|
|
131
143
|
|
|
132
144
|
| Setting | Type | Default | Description |
|
package/docs/skills.md
CHANGED
|
@@ -34,8 +34,9 @@ Pi loads skills from:
|
|
|
34
34
|
- CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)
|
|
35
35
|
|
|
36
36
|
Discovery rules:
|
|
37
|
-
-
|
|
38
|
-
-
|
|
37
|
+
- In `~/.pi/agent/skills/` and `.pi/skills/`, direct root `.md` files are discovered as individual skills
|
|
38
|
+
- In all skill locations, directories containing `SKILL.md` are discovered recursively
|
|
39
|
+
- In `~/.agents/skills/` and project `.agents/skills/`, root `.md` files are ignored
|
|
39
40
|
|
|
40
41
|
Disable discovery with `--no-skills` (explicit `--skill` paths still load).
|
|
41
42
|
|
package/docs/tui.md
CHANGED
|
@@ -394,7 +394,7 @@ Components accept theme objects for styling.
|
|
|
394
394
|
**In `renderCall`/`renderResult`**, use the `theme` parameter:
|
|
395
395
|
|
|
396
396
|
```typescript
|
|
397
|
-
renderResult(result, options, theme) {
|
|
397
|
+
renderResult(result, options, theme, context) {
|
|
398
398
|
// Use theme.fg() for foreground colors
|
|
399
399
|
return new Text(theme.fg("success", "Done!"), 0, 0);
|
|
400
400
|
|
|
@@ -428,7 +428,7 @@ renderResult(result, options, theme) {
|
|
|
428
428
|
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
|
429
429
|
import { Markdown } from "@mariozechner/pi-tui";
|
|
430
430
|
|
|
431
|
-
renderResult(result, options, theme) {
|
|
431
|
+
renderResult(result, options, theme, context) {
|
|
432
432
|
const mdTheme = getMarkdownTheme();
|
|
433
433
|
return new Markdown(result.details.markdown, 0, 0, mdTheme);
|
|
434
434
|
}
|