gsd-pi 2.65.0-dev.5c8557b → 2.65.0-dev.d0517ff
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/dist/mcp-server.js +6 -2
- package/dist/resources/extensions/browser-tools/capture.js +20 -1
- package/dist/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +93 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +13 -2
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +99 -9
- package/dist/resources/extensions/gsd/auto-model-selection.js +7 -5
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -6
- package/dist/resources/extensions/gsd/auto-prompts.js +24 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +40 -22
- package/dist/resources/extensions/gsd/auto-start.js +42 -11
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +10 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +29 -7
- package/dist/resources/extensions/gsd/auto.js +21 -15
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -4
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +10 -0
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +6 -4
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +5 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -3
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +31 -1
- package/dist/resources/extensions/gsd/commands/context.js +8 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +20 -0
- package/dist/resources/extensions/gsd/commands-extensions.js +1 -1
- package/dist/resources/extensions/gsd/config-overlay.js +312 -0
- package/dist/resources/extensions/gsd/db-writer.js +13 -3
- package/dist/resources/extensions/gsd/detection.js +1 -1
- package/dist/resources/extensions/gsd/dispatch-guard.js +2 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/dist/resources/extensions/gsd/doctor.js +2 -1
- package/dist/resources/extensions/gsd/gitignore.js +1 -0
- package/dist/resources/extensions/gsd/gsd-db.js +47 -4
- package/dist/resources/extensions/gsd/guided-flow.js +220 -29
- package/dist/resources/extensions/gsd/index.js +1 -1
- package/dist/resources/extensions/gsd/json-persistence.js +5 -2
- package/dist/resources/extensions/gsd/md-importer.js +14 -7
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +17 -11
- package/dist/resources/extensions/gsd/pre-execution-checks.js +12 -5
- package/dist/resources/extensions/gsd/preferences-types.js +3 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +45 -1
- package/dist/resources/extensions/gsd/preferences.js +9 -2
- package/dist/resources/extensions/gsd/preparation.js +1092 -0
- package/dist/resources/extensions/gsd/prompt-validation.js +67 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +3 -3
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +424 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -4
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +23 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -0
- package/dist/resources/extensions/gsd/prompts/rethink.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +56 -23
- package/dist/resources/extensions/gsd/quick.js +19 -15
- package/dist/resources/extensions/gsd/reactive-graph.js +12 -0
- package/dist/resources/extensions/gsd/roadmap-slices.js +24 -5
- package/dist/resources/extensions/gsd/safety/content-validator.js +3 -3
- package/dist/resources/extensions/gsd/session-lock.js +23 -1
- package/dist/resources/extensions/gsd/state.js +115 -28
- package/dist/resources/extensions/gsd/templates/context-enhanced.md +138 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +15 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +27 -6
- package/dist/resources/extensions/gsd/tools/complete-task.js +31 -7
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +7 -5
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +5 -2
- package/dist/resources/extensions/gsd/tools/reopen-milestone.js +119 -0
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +30 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +18 -0
- package/dist/resources/extensions/gsd/triage-resolution.js +33 -16
- package/dist/resources/extensions/gsd/undo.js +3 -2
- package/dist/resources/extensions/gsd/workflow-events.js +1 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-projections.js +7 -9
- package/dist/resources/extensions/gsd/workflow-reconcile.js +100 -9
- package/dist/resources/extensions/gsd/workflow-templates.js +11 -2
- package/dist/resources/extensions/gsd/worktree-manager.js +5 -2
- package/dist/resources/extensions/gsd/worktree.js +9 -0
- package/dist/resources/extensions/shared/interview-ui.js +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/6502.8874bcae249c02e1.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-a1c1e452c6b32d04.js → webpack-9fed74684e1c5bb1.js} +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +30 -19
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +51 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +9 -9
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +10 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +20 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +15 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js +18 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +80 -0
- package/packages/pi-coding-agent/src/core/retry-handler.ts +37 -25
- package/packages/pi-coding-agent/src/core/sdk.ts +9 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +20 -4
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +27 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +16 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
- package/packages/pi-tui/dist/components/image.d.ts +2 -0
- package/packages/pi-tui/dist/components/image.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/image.js +4 -0
- package/packages/pi-tui/dist/components/image.js.map +1 -1
- package/packages/pi-tui/dist/components/image.test.d.ts +6 -0
- package/packages/pi-tui/dist/components/image.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/image.test.js +32 -0
- package/packages/pi-tui/dist/components/image.test.js.map +1 -0
- package/packages/pi-tui/src/components/image.test.ts +36 -0
- package/packages/pi-tui/src/components/image.ts +5 -0
- package/src/resources/extensions/browser-tools/capture.ts +19 -1
- package/src/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +93 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +12 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +110 -9
- package/src/resources/extensions/gsd/auto-model-selection.ts +7 -5
- package/src/resources/extensions/gsd/auto-post-unit.ts +16 -6
- package/src/resources/extensions/gsd/auto-prompts.ts +31 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -23
- package/src/resources/extensions/gsd/auto-start.ts +45 -10
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +10 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +28 -7
- package/src/resources/extensions/gsd/auto.ts +19 -8
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +16 -4
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +10 -0
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +4 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -3
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +36 -1
- package/src/resources/extensions/gsd/commands/context.ts +7 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +23 -0
- package/src/resources/extensions/gsd/commands-extensions.ts +1 -1
- package/src/resources/extensions/gsd/config-overlay.ts +331 -0
- package/src/resources/extensions/gsd/db-writer.ts +11 -3
- package/src/resources/extensions/gsd/detection.ts +1 -1
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/src/resources/extensions/gsd/doctor.ts +2 -1
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/gsd-db.ts +46 -4
- package/src/resources/extensions/gsd/guided-flow.ts +254 -30
- package/src/resources/extensions/gsd/index.ts +1 -0
- package/src/resources/extensions/gsd/json-persistence.ts +6 -3
- package/src/resources/extensions/gsd/md-importer.ts +13 -6
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +19 -11
- package/src/resources/extensions/gsd/pre-execution-checks.ts +15 -7
- package/src/resources/extensions/gsd/preferences-types.ts +25 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +45 -1
- package/src/resources/extensions/gsd/preferences.ts +9 -2
- package/src/resources/extensions/gsd/preparation.ts +1419 -0
- package/src/resources/extensions/gsd/prompt-validation.ts +88 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +3 -3
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-prepared.md +424 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -4
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +23 -0
- package/src/resources/extensions/gsd/prompts/queue.md +2 -0
- package/src/resources/extensions/gsd/prompts/rethink.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +56 -23
- package/src/resources/extensions/gsd/quick.ts +20 -15
- package/src/resources/extensions/gsd/reactive-graph.ts +18 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +21 -5
- package/src/resources/extensions/gsd/safety/content-validator.ts +3 -3
- package/src/resources/extensions/gsd/session-lock.ts +17 -1
- package/src/resources/extensions/gsd/state.ts +115 -26
- package/src/resources/extensions/gsd/templates/context-enhanced.md +138 -0
- package/src/resources/extensions/gsd/tests/adversarial-review-fixes.test.ts +223 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +33 -2
- package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/complete-task-normalize-lists.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/defer-milestone-stamp.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard-closed-status.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/error-success-mask.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/frontmatter-parse-noise.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/gitignore-bg-shell.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +11 -9
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +28 -30
- package/src/resources/extensions/gsd/tests/integration/test-isolation.ts +53 -0
- package/src/resources/extensions/gsd/tests/integration-prepared-discussion.test.ts +525 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/needs-remediation-revalidation.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/note-captures-executed.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/phantom-ghost-detection.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/phantom-milestone-default-queued.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +218 -20
- package/src/resources/extensions/gsd/tests/pre-execution-fail-closed.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/pre-execution-pause-wiring.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/preparation.test.ts +1211 -0
- package/src/resources/extensions/gsd/tests/project-root-cwd-crash.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/projection-no-plan-overwrite.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/prompt-builder.test.ts +669 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +7 -4
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +4 -5
- package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/show-config-command.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/skip-slice-state-rebuild.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/slice-sequence-insert.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/stale-lockfile-recovery.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +6 -7
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/stuck-detection-coverage.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/symlink-extension-discovery.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +89 -0
- package/src/resources/extensions/gsd/tests/wave1-critical-regressions.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/wave2-events-regressions.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/wave3-session-regressions.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/wave4-write-safety-regressions.test.ts +70 -0
- package/src/resources/extensions/gsd/tests/wave5-consistency-regressions.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/worker-model-override.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/workflow-logger-audit.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/worktree-expected-warnings.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/worktree-main-branch.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +16 -17
- package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +13 -9
- package/src/resources/extensions/gsd/tests/worktree.test.ts +26 -9
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +127 -2
- package/src/resources/extensions/gsd/tests/zero-slice-roadmap-guided.test.ts +19 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +13 -3
- package/src/resources/extensions/gsd/tools/complete-slice.ts +26 -6
- package/src/resources/extensions/gsd/tools/complete-task.ts +29 -7
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +11 -9
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +5 -2
- package/src/resources/extensions/gsd/tools/reopen-milestone.ts +152 -0
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +27 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +17 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +37 -17
- package/src/resources/extensions/gsd/types.ts +4 -0
- package/src/resources/extensions/gsd/undo.ts +3 -2
- package/src/resources/extensions/gsd/workflow-events.ts +5 -3
- package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-projections.ts +7 -8
- package/src/resources/extensions/gsd/workflow-reconcile.ts +109 -8
- package/src/resources/extensions/gsd/workflow-templates.ts +11 -2
- package/src/resources/extensions/gsd/worktree-manager.ts +4 -2
- package/src/resources/extensions/gsd/worktree.ts +10 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +8 -10
- package/dist/web/standalone/.next/static/chunks/6502.7593d7797a4b3999.js +0 -9
- /package/dist/web/standalone/.next/static/{qq3YfHPfyqvh3DIMVmsRH → JwdBI3y1H8vtBKiYvWfEK}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{qq3YfHPfyqvh3DIMVmsRH → JwdBI3y1H8vtBKiYvWfEK}/_ssgManifest.js +0 -0
|
@@ -278,5 +278,85 @@ describe("RetryHandler — long-context entitlement 429 (#2803)", () => {
|
|
|
278
278
|
const msg = errorMessage("Extra usage is required for long context requests.");
|
|
279
279
|
assert.equal(handler.isRetryableError(msg), true);
|
|
280
280
|
});
|
|
281
|
+
|
|
282
|
+
it("does NOT consider credential cooldown error as retryable (#3429)", () => {
|
|
283
|
+
// The credential cooldown message from getApiKey() must not re-enter
|
|
284
|
+
// the retry handler. Re-entry creates cascading empty error entries
|
|
285
|
+
// in the session file that break resume.
|
|
286
|
+
const { deps } = createMockDeps();
|
|
287
|
+
const handler = new RetryHandler(deps);
|
|
288
|
+
const msg = errorMessage(
|
|
289
|
+
'All credentials for "anthropic" are in a cooldown window. ' +
|
|
290
|
+
'Please wait a moment and try again, or switch to a different provider.',
|
|
291
|
+
);
|
|
292
|
+
assert.equal(handler.isRetryableError(msg), false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("quota_exhausted credential backoff (#3430)", () => {
|
|
297
|
+
it("does NOT call markUsageLimitReached for quota_exhausted errors", async () => {
|
|
298
|
+
// "Extra usage is required" is an account-level billing gate.
|
|
299
|
+
// Backing off the credential for 30 minutes blocks all provider
|
|
300
|
+
// requests and has no effect on the billing condition.
|
|
301
|
+
const { deps, markUsageLimitReached } = createMockDeps({
|
|
302
|
+
model: createMockModel("anthropic", "claude-opus-4-6[1m]"),
|
|
303
|
+
markUsageLimitReachedResult: false,
|
|
304
|
+
fallbackResult: null,
|
|
305
|
+
findModelResult: () => undefined,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const handler = new RetryHandler(deps);
|
|
309
|
+
const msg = errorMessage(
|
|
310
|
+
'429 {"type":"error","error":{"type":"rate_limit_error","message":"Extra usage is required for long context requests."}}',
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await handler.handleRetryableError(msg);
|
|
314
|
+
|
|
315
|
+
assert.equal(
|
|
316
|
+
markUsageLimitReached.mock.calls.length,
|
|
317
|
+
0,
|
|
318
|
+
"markUsageLimitReached must NOT be called for quota_exhausted errors",
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("still calls markUsageLimitReached for regular rate_limit errors", async () => {
|
|
323
|
+
const { deps, markUsageLimitReached } = createMockDeps({
|
|
324
|
+
model: createMockModel("anthropic", "claude-opus-4-6"),
|
|
325
|
+
markUsageLimitReachedResult: false,
|
|
326
|
+
fallbackResult: null,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const handler = new RetryHandler(deps);
|
|
330
|
+
const msg = errorMessage("429 Too Many Requests");
|
|
331
|
+
|
|
332
|
+
await handler.handleRetryableError(msg);
|
|
333
|
+
|
|
334
|
+
assert.equal(
|
|
335
|
+
markUsageLimitReached.mock.calls.length,
|
|
336
|
+
1,
|
|
337
|
+
"markUsageLimitReached should be called for rate_limit errors",
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("still tries cross-provider fallback for quota_exhausted without credential backoff", async () => {
|
|
342
|
+
const fallbackModel = createMockModel("openai", "gpt-4o");
|
|
343
|
+
const { deps, markUsageLimitReached, continueFn } = createMockDeps({
|
|
344
|
+
model: createMockModel("anthropic", "claude-opus-4-6[1m]"),
|
|
345
|
+
markUsageLimitReachedResult: false,
|
|
346
|
+
fallbackResult: { model: fallbackModel, reason: "cross-provider fallback" },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const handler = new RetryHandler(deps);
|
|
350
|
+
const msg = errorMessage("Extra usage is required for long context requests.");
|
|
351
|
+
|
|
352
|
+
const result = await handler.handleRetryableError(msg);
|
|
353
|
+
|
|
354
|
+
assert.equal(result, true, "should retry with fallback provider");
|
|
355
|
+
assert.equal(
|
|
356
|
+
markUsageLimitReached.mock.calls.length,
|
|
357
|
+
0,
|
|
358
|
+
"should NOT back off credentials before trying fallback",
|
|
359
|
+
);
|
|
360
|
+
});
|
|
281
361
|
});
|
|
282
362
|
});
|
|
@@ -109,7 +109,11 @@ export class RetryHandler {
|
|
|
109
109
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
110
110
|
|
|
111
111
|
const err = message.errorMessage;
|
|
112
|
-
|
|
112
|
+
// "temporarily backed off" is intentionally excluded: it is an internally-
|
|
113
|
+
// generated error from getApiKey() when credentials are in a backoff window.
|
|
114
|
+
// Re-entering the retry handler for that message creates a cascade of empty
|
|
115
|
+
// error entries in the session file, breaking resume (#3429).
|
|
116
|
+
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|extra usage is required/i.test(
|
|
113
117
|
err,
|
|
114
118
|
);
|
|
115
119
|
}
|
|
@@ -139,34 +143,42 @@ export class RetryHandler {
|
|
|
139
143
|
const retryGeneration = this._retryGeneration;
|
|
140
144
|
if (this._deps.getModel() && message.errorMessage) {
|
|
141
145
|
const errorType = this._classifyErrorType(message.errorMessage);
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
const isRateLimit = errorType === "rate_limit";
|
|
147
|
+
const isQuotaError = errorType === "quota_exhausted";
|
|
148
|
+
|
|
149
|
+
// Credential rotation — only for transient rate limits (#3430).
|
|
150
|
+
// Quota errors ("Extra usage is required") are account-level billing
|
|
151
|
+
// gates; rotating to another credential on the same account won't help
|
|
152
|
+
// and the 30-minute backoff blocks all provider requests needlessly.
|
|
153
|
+
if (isRateLimit) {
|
|
154
|
+
const hasAlternate =
|
|
155
|
+
this._deps.modelRegistry.authStorage.markUsageLimitReached(
|
|
156
|
+
this._deps.getModel()!.provider,
|
|
157
|
+
this._deps.getSessionId(),
|
|
158
|
+
{ errorType },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (hasAlternate) {
|
|
162
|
+
this._removeLastAssistantError();
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
this._deps.emit({
|
|
165
|
+
type: "auto_retry_start",
|
|
166
|
+
attempt: this._retryAttempt + 1,
|
|
167
|
+
maxAttempts: settings.maxRetries,
|
|
168
|
+
delayMs: 0,
|
|
169
|
+
errorMessage: `${message.errorMessage} (switching credential)`,
|
|
170
|
+
});
|
|
161
171
|
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
// Retry immediately with the next credential - don't increment _retryAttempt
|
|
173
|
+
this._scheduleContinue(retryGeneration);
|
|
164
174
|
|
|
165
|
-
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
//
|
|
169
|
-
|
|
179
|
+
// Cross-provider fallback — for rate limits with all creds backed off,
|
|
180
|
+
// or quota errors (which skip credential backoff entirely).
|
|
181
|
+
if (isRateLimit || isQuotaError) {
|
|
170
182
|
const fallbackResult = await this._deps.fallbackResolver.findFallback(
|
|
171
183
|
this._deps.getModel()!,
|
|
172
184
|
errorType,
|
|
@@ -200,7 +212,7 @@ export class RetryHandler {
|
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
// No fallback available either
|
|
203
|
-
if (
|
|
215
|
+
if (isQuotaError) {
|
|
204
216
|
// Try long-context model downgrade ([1m] → base) before giving up
|
|
205
217
|
const downgraded = this._tryLongContextDowngrade(message, retryGeneration);
|
|
206
218
|
if (downgraded) return true;
|
|
@@ -371,16 +371,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
371
371
|
await new Promise(resolve => setTimeout(resolve, baseDelayMs * attempt));
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
// All retries exhausted — throw descriptive error
|
|
375
|
-
// Check if credentials exist but are temporarily
|
|
376
|
-
// (e.g., after a 429
|
|
377
|
-
//
|
|
378
|
-
//
|
|
374
|
+
// All retries exhausted — throw descriptive error.
|
|
375
|
+
// Check if credentials exist but are temporarily in a backoff window
|
|
376
|
+
// (e.g., after a 429). This message intentionally avoids phrases like
|
|
377
|
+
// "rate limit" / "429" to prevent isRetryableError() from re-entering
|
|
378
|
+
// the retry handler and creating cascading error entries (#3429).
|
|
379
379
|
const hasAuth = modelRegistry.authStorage.hasAuth(resolvedProvider);
|
|
380
380
|
if (hasAuth) {
|
|
381
381
|
throw new Error(
|
|
382
|
-
`All credentials for "${resolvedProvider}" are
|
|
383
|
-
`
|
|
382
|
+
`All credentials for "${resolvedProvider}" are in a cooldown window. ` +
|
|
383
|
+
`Please wait a moment and try again, or switch to a different provider.`,
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
386
|
const model = agent.state.model;
|
|
@@ -390,8 +390,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
390
390
|
// surface a specific message instead of the misleading "Authentication failed".
|
|
391
391
|
if (modelRegistry.authStorage.areAllCredentialsBackedOff(resolvedProvider)) {
|
|
392
392
|
throw new Error(
|
|
393
|
-
`
|
|
394
|
-
`Please wait
|
|
393
|
+
`All credentials for "${resolvedProvider}" are in a cooldown window. ` +
|
|
394
|
+
`Please wait a moment and try again, or switch to a different provider.`,
|
|
395
395
|
);
|
|
396
396
|
}
|
|
397
397
|
throw new Error(
|
|
@@ -43,6 +43,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
43
43
|
private modelsJsonWriter: ModelsJsonWriter;
|
|
44
44
|
private onDone: () => void;
|
|
45
45
|
private onDiscover: (provider: string) => void;
|
|
46
|
+
private onSetupAuth: (provider: string) => void;
|
|
46
47
|
private confirmingRemove = false;
|
|
47
48
|
private hintsContainer: Container;
|
|
48
49
|
|
|
@@ -52,6 +53,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
52
53
|
modelRegistry: ModelRegistry,
|
|
53
54
|
onDone: () => void,
|
|
54
55
|
onDiscover: (provider: string) => void,
|
|
56
|
+
onSetupAuth?: (provider: string) => void,
|
|
55
57
|
) {
|
|
56
58
|
super();
|
|
57
59
|
|
|
@@ -61,6 +63,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
61
63
|
this.modelsJsonWriter = new ModelsJsonWriter(this.modelRegistry.modelsJsonPath);
|
|
62
64
|
this.onDone = onDone;
|
|
63
65
|
this.onDiscover = onDiscover;
|
|
66
|
+
this.onSetupAuth = onSetupAuth ?? (() => {});
|
|
64
67
|
|
|
65
68
|
// Header
|
|
66
69
|
this.addChild(new Text(theme.fg("accent", "Provider Manager"), 0, 0));
|
|
@@ -125,6 +128,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
125
128
|
this.hintsContainer.addChild(new Text(hints, 0, 0));
|
|
126
129
|
} else {
|
|
127
130
|
const hints = [
|
|
131
|
+
rawKeyHint("enter", "setup auth"),
|
|
128
132
|
rawKeyHint("d", "discover"),
|
|
129
133
|
rawKeyHint("r", "remove auth"),
|
|
130
134
|
rawKeyHint("esc", "close"),
|
|
@@ -203,6 +207,12 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
203
207
|
this.tui.requestRender();
|
|
204
208
|
}
|
|
205
209
|
}
|
|
210
|
+
} else if (kb.matches(keyData, "selectConfirm")) {
|
|
211
|
+
// Enter key → initiate auth setup for the selected provider (#3579)
|
|
212
|
+
const provider = this.providers[this.selectedIndex];
|
|
213
|
+
if (provider) {
|
|
214
|
+
this.onSetupAuth(provider.name);
|
|
215
|
+
}
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
218
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Container,
|
|
4
4
|
getCapabilities,
|
|
5
5
|
Image,
|
|
6
|
+
type ImageDimensions,
|
|
6
7
|
imageFallback,
|
|
7
8
|
Spacer,
|
|
8
9
|
Text,
|
|
@@ -88,6 +89,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
88
89
|
private editDiffArgsKey?: string; // Track which args the preview is for
|
|
89
90
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
90
91
|
private convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
92
|
+
// Cached resolved image dimensions to avoid re-triggering async parsing
|
|
93
|
+
// when updateDisplay() recreates Image components (#3455).
|
|
94
|
+
private resolvedImageDimensions: Map<number, ImageDimensions> = new Map();
|
|
91
95
|
// Incremental syntax highlighting cache for write tool call args
|
|
92
96
|
private writeHighlightCache?: WriteHighlightCache;
|
|
93
97
|
// When true, this component intentionally renders no lines
|
|
@@ -481,16 +485,28 @@ export class ToolExecutionComponent extends Container {
|
|
|
481
485
|
const spacer = new Spacer(1);
|
|
482
486
|
this.addChild(spacer);
|
|
483
487
|
this.imageSpacers.push(spacer);
|
|
488
|
+
// Pass cached dimensions to avoid re-triggering async parsing
|
|
489
|
+
// when updateDisplay() recreates Image components (#3455).
|
|
490
|
+
const cachedDims = this.resolvedImageDimensions.get(i);
|
|
484
491
|
const imageComponent = new Image(
|
|
485
492
|
imageData,
|
|
486
493
|
imageMimeType,
|
|
487
494
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
488
495
|
{ maxWidthCells: 60 },
|
|
496
|
+
cachedDims,
|
|
489
497
|
);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
498
|
+
if (!cachedDims) {
|
|
499
|
+
const imgIdx = i;
|
|
500
|
+
imageComponent.setOnDimensionsResolved(() => {
|
|
501
|
+
// Cache resolved dimensions so future updateDisplay() calls
|
|
502
|
+
// don't re-trigger async parsing → infinite loop (#3455).
|
|
503
|
+
const dims = imageComponent.getDimensions?.();
|
|
504
|
+
if (dims) this.resolvedImageDimensions.set(imgIdx, dims);
|
|
505
|
+
// Just re-render — don't call updateDisplay() which would
|
|
506
|
+
// destroy and recreate all Image components.
|
|
507
|
+
this.ui.requestRender();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
494
510
|
this.imageComponents.push(imageComponent);
|
|
495
511
|
this.addChild(imageComponent);
|
|
496
512
|
}
|
|
@@ -154,3 +154,30 @@ test("input-controller: truly unknown slash commands stop before session.prompt"
|
|
|
154
154
|
);
|
|
155
155
|
assert.equal(getEditorText(), "", "unknown slash commands should clear the editor after showing the error");
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
test("input-controller: absolute file paths are not treated as slash commands (#3478)", async () => {
|
|
159
|
+
const { host, prompted, errors } = createHost();
|
|
160
|
+
|
|
161
|
+
await host.defaultEditor.onSubmit("/Users/name/Desktop/screenshot.png");
|
|
162
|
+
|
|
163
|
+
assert.deepEqual(errors, [], "file paths should not trigger unknown command error");
|
|
164
|
+
assert.deepEqual(prompted, ["/Users/name/Desktop/screenshot.png"], "file paths should be sent as plain input");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("input-controller: Linux absolute paths are not treated as slash commands (#3478)", async () => {
|
|
168
|
+
const { host, prompted, errors } = createHost();
|
|
169
|
+
|
|
170
|
+
await host.defaultEditor.onSubmit("/home/user/documents/file.txt");
|
|
171
|
+
|
|
172
|
+
assert.deepEqual(errors, [], "Linux paths should not trigger unknown command error");
|
|
173
|
+
assert.deepEqual(prompted, ["/home/user/documents/file.txt"], "Linux paths should be sent as plain input");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("input-controller: /tmp paths are not treated as slash commands (#3478)", async () => {
|
|
177
|
+
const { host, prompted, errors } = createHost();
|
|
178
|
+
|
|
179
|
+
await host.defaultEditor.onSubmit("/tmp/some-file.log");
|
|
180
|
+
|
|
181
|
+
assert.deepEqual(errors, []);
|
|
182
|
+
assert.deepEqual(prompted, ["/tmp/some-file.log"]);
|
|
183
|
+
});
|
|
@@ -18,7 +18,7 @@ export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
|
|
|
18
18
|
text = text.trim();
|
|
19
19
|
if (!text) return;
|
|
20
20
|
|
|
21
|
-
if (text.startsWith("/")) {
|
|
21
|
+
if (text.startsWith("/") && !looksLikeFilePath(text)) {
|
|
22
22
|
const handled = await dispatchSlashCommand(text, host.getSlashCommandContext());
|
|
23
23
|
if (handled) {
|
|
24
24
|
host.editor.setText("");
|
|
@@ -104,3 +104,18 @@ export function setupEditorSubmitHandler(host: InteractiveModeStateHost & {
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Distinguish absolute file paths from slash commands (#3478).
|
|
110
|
+
* Drag-and-drop inserts paths like "/Users/name/Desktop/file.png" which
|
|
111
|
+
* should be treated as plain text input, not a /Users command.
|
|
112
|
+
*
|
|
113
|
+
* Heuristic: a slash command is a single token like "/help" or "/gsd auto".
|
|
114
|
+
* File paths have a second "/" within the first token (e.g., "/Users/...").
|
|
115
|
+
*/
|
|
116
|
+
function looksLikeFilePath(text: string): boolean {
|
|
117
|
+
const firstToken = text.split(/\s/)[0];
|
|
118
|
+
// Slash commands: /help, /gsd, /commit — single "/" at start only.
|
|
119
|
+
// File paths: /Users/name/file, /home/user/file, /tmp/x — contain "/" after position 0.
|
|
120
|
+
return firstToken.indexOf("/", 1) !== -1;
|
|
121
|
+
}
|
|
@@ -3411,6 +3411,11 @@ export class InteractiveMode {
|
|
|
3411
3411
|
done();
|
|
3412
3412
|
this.ui.requestRender();
|
|
3413
3413
|
},
|
|
3414
|
+
async (provider: string) => {
|
|
3415
|
+
// Enter key → auth setup for selected provider (#3579)
|
|
3416
|
+
done();
|
|
3417
|
+
await this.showLoginDialog(provider);
|
|
3418
|
+
},
|
|
3414
3419
|
);
|
|
3415
3420
|
return { component, focus: component };
|
|
3416
3421
|
});
|
|
@@ -29,6 +29,8 @@ export declare class Image implements Component {
|
|
|
29
29
|
setOnDimensionsResolved(cb: () => void): void;
|
|
30
30
|
/** Get the Kitty image ID used by this image (if any). */
|
|
31
31
|
getImageId(): number | undefined;
|
|
32
|
+
/** Get the resolved image dimensions (for caching across recreations). */
|
|
33
|
+
getDimensions(): ImageDimensions | undefined;
|
|
32
34
|
invalidate(): void;
|
|
33
35
|
render(width: number): string[];
|
|
34
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,eAAe,EAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,UAAU;IAC1B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,KAAM,YAAW,SAAS;IACtC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,oBAAoB,CAAC,CAAa;IAE1C,OAAO,CAAC,WAAW,CAAC,CAAW;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;gBAG5B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,YAAiB,EAC1B,UAAU,CAAC,EAAE,eAAe;IAsB7B;;;OAGG;IACH,uBAAuB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAI7C,0DAA0D;IAC1D,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,UAAU,IAAI,IAAI;IAKlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CA8C/B"}
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,eAAe,EAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,UAAU;IAC1B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,KAAM,YAAW,SAAS;IACtC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,oBAAoB,CAAC,CAAa;IAE1C,OAAO,CAAC,WAAW,CAAC,CAAW;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;gBAG5B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,YAAiB,EAC1B,UAAU,CAAC,EAAE,eAAe;IAsB7B;;;OAGG;IACH,uBAAuB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAI7C,0DAA0D;IAC1D,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,0EAA0E;IAC1E,aAAa,IAAI,eAAe,GAAG,SAAS;IAI5C,UAAU,IAAI,IAAI;IAKlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CA8C/B"}
|
|
@@ -31,6 +31,10 @@ export class Image {
|
|
|
31
31
|
getImageId() {
|
|
32
32
|
return this.imageId;
|
|
33
33
|
}
|
|
34
|
+
/** Get the resolved image dimensions (for caching across recreations). */
|
|
35
|
+
getDimensions() {
|
|
36
|
+
return this.dimensionsResolved ? this.dimensions : undefined;
|
|
37
|
+
}
|
|
34
38
|
invalidate() {
|
|
35
39
|
this.cachedLines = undefined;
|
|
36
40
|
this.cachedWidth = undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tgetCapabilities,\n\tgetImageDimensions,\n\ttype ImageDimensions,\n\timageFallback,\n\trenderImage,\n} from \"../terminal-image.js\";\nimport type { Component } from \"../tui.js\";\n\nexport interface ImageTheme {\n\tfallbackColor: (str: string) => string;\n}\n\nexport interface ImageOptions {\n\tmaxWidthCells?: number;\n\tmaxHeightCells?: number;\n\tfilename?: string;\n\t/** Kitty image ID. If provided, reuses this ID (for animations/updates). */\n\timageId?: number;\n}\n\nexport class Image implements Component {\n\tprivate base64Data: string;\n\tprivate mimeType: string;\n\tprivate dimensions: ImageDimensions;\n\tprivate theme: ImageTheme;\n\tprivate options: ImageOptions;\n\tprivate imageId?: number;\n\tprivate dimensionsResolved = false;\n\tprivate onDimensionsResolved?: () => void;\n\n\tprivate cachedLines?: string[];\n\tprivate cachedWidth?: number;\n\n\tconstructor(\n\t\tbase64Data: string,\n\t\tmimeType: string,\n\t\ttheme: ImageTheme,\n\t\toptions: ImageOptions = {},\n\t\tdimensions?: ImageDimensions,\n\t) {\n\t\tthis.base64Data = base64Data;\n\t\tthis.mimeType = mimeType;\n\t\tthis.theme = theme;\n\t\tthis.options = options;\n\t\tthis.dimensions = dimensions || { widthPx: 800, heightPx: 600 };\n\t\tthis.dimensionsResolved = !!dimensions;\n\t\tthis.imageId = options.imageId;\n\n\t\tif (!dimensions) {\n\t\t\tgetImageDimensions(base64Data).then((dims) => {\n\t\t\t\tif (dims) {\n\t\t\t\t\tthis.dimensions = dims;\n\t\t\t\t\tthis.dimensionsResolved = true;\n\t\t\t\t\tthis.invalidate();\n\t\t\t\t\tthis.onDimensionsResolved?.();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Register a callback invoked when async dimension parsing completes.\n\t * Useful for triggering a re-render after the Image updates its layout.\n\t */\n\tsetOnDimensionsResolved(cb: () => void): void {\n\t\tthis.onDimensionsResolved = cb;\n\t}\n\n\t/** Get the Kitty image ID used by this image (if any). */\n\tgetImageId(): number | undefined {\n\t\treturn this.imageId;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedLines = undefined;\n\t\tthis.cachedWidth = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tconst maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60);\n\n\t\tconst caps = getCapabilities();\n\t\tlet lines: string[];\n\n\t\tif (caps.images) {\n\t\t\tconst result = renderImage(this.base64Data, this.dimensions, {\n\t\t\t\tmaxWidthCells: maxWidth,\n\t\t\t\timageId: this.imageId,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\t// Store the image ID for later cleanup\n\t\t\t\tif (result.imageId) {\n\t\t\t\t\tthis.imageId = result.imageId;\n\t\t\t\t}\n\n\t\t\t\t// Return `rows` lines so TUI accounts for image height\n\t\t\t\t// First (rows-1) lines are empty (TUI clears them)\n\t\t\t\t// Last line: move cursor back up, then output image sequence\n\t\t\t\tlines = [];\n\t\t\t\tfor (let i = 0; i < result.rows - 1; i++) {\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t}\n\t\t\t\t// Move cursor up to first row, then output image\n\t\t\t\tconst moveUp = result.rows > 1 ? `\\x1b[${result.rows - 1}A` : \"\";\n\t\t\t\tlines.push(moveUp + result.sequence);\n\t\t\t} else {\n\t\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t\t}\n\t\t} else {\n\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t}\n\n\t\tthis.cachedLines = lines;\n\t\tthis.cachedWidth = width;\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tgetCapabilities,\n\tgetImageDimensions,\n\ttype ImageDimensions,\n\timageFallback,\n\trenderImage,\n} from \"../terminal-image.js\";\nimport type { Component } from \"../tui.js\";\n\nexport interface ImageTheme {\n\tfallbackColor: (str: string) => string;\n}\n\nexport interface ImageOptions {\n\tmaxWidthCells?: number;\n\tmaxHeightCells?: number;\n\tfilename?: string;\n\t/** Kitty image ID. If provided, reuses this ID (for animations/updates). */\n\timageId?: number;\n}\n\nexport class Image implements Component {\n\tprivate base64Data: string;\n\tprivate mimeType: string;\n\tprivate dimensions: ImageDimensions;\n\tprivate theme: ImageTheme;\n\tprivate options: ImageOptions;\n\tprivate imageId?: number;\n\tprivate dimensionsResolved = false;\n\tprivate onDimensionsResolved?: () => void;\n\n\tprivate cachedLines?: string[];\n\tprivate cachedWidth?: number;\n\n\tconstructor(\n\t\tbase64Data: string,\n\t\tmimeType: string,\n\t\ttheme: ImageTheme,\n\t\toptions: ImageOptions = {},\n\t\tdimensions?: ImageDimensions,\n\t) {\n\t\tthis.base64Data = base64Data;\n\t\tthis.mimeType = mimeType;\n\t\tthis.theme = theme;\n\t\tthis.options = options;\n\t\tthis.dimensions = dimensions || { widthPx: 800, heightPx: 600 };\n\t\tthis.dimensionsResolved = !!dimensions;\n\t\tthis.imageId = options.imageId;\n\n\t\tif (!dimensions) {\n\t\t\tgetImageDimensions(base64Data).then((dims) => {\n\t\t\t\tif (dims) {\n\t\t\t\t\tthis.dimensions = dims;\n\t\t\t\t\tthis.dimensionsResolved = true;\n\t\t\t\t\tthis.invalidate();\n\t\t\t\t\tthis.onDimensionsResolved?.();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Register a callback invoked when async dimension parsing completes.\n\t * Useful for triggering a re-render after the Image updates its layout.\n\t */\n\tsetOnDimensionsResolved(cb: () => void): void {\n\t\tthis.onDimensionsResolved = cb;\n\t}\n\n\t/** Get the Kitty image ID used by this image (if any). */\n\tgetImageId(): number | undefined {\n\t\treturn this.imageId;\n\t}\n\n\t/** Get the resolved image dimensions (for caching across recreations). */\n\tgetDimensions(): ImageDimensions | undefined {\n\t\treturn this.dimensionsResolved ? this.dimensions : undefined;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedLines = undefined;\n\t\tthis.cachedWidth = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tconst maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60);\n\n\t\tconst caps = getCapabilities();\n\t\tlet lines: string[];\n\n\t\tif (caps.images) {\n\t\t\tconst result = renderImage(this.base64Data, this.dimensions, {\n\t\t\t\tmaxWidthCells: maxWidth,\n\t\t\t\timageId: this.imageId,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\t// Store the image ID for later cleanup\n\t\t\t\tif (result.imageId) {\n\t\t\t\t\tthis.imageId = result.imageId;\n\t\t\t\t}\n\n\t\t\t\t// Return `rows` lines so TUI accounts for image height\n\t\t\t\t// First (rows-1) lines are empty (TUI clears them)\n\t\t\t\t// Last line: move cursor back up, then output image sequence\n\t\t\t\tlines = [];\n\t\t\t\tfor (let i = 0; i < result.rows - 1; i++) {\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t}\n\t\t\t\t// Move cursor up to first row, then output image\n\t\t\t\tconst moveUp = result.rows > 1 ? `\\x1b[${result.rows - 1}A` : \"\";\n\t\t\t\tlines.push(moveUp + result.sequence);\n\t\t\t} else {\n\t\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t\t}\n\t\t} else {\n\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t}\n\n\t\tthis.cachedLines = lines;\n\t\tthis.cachedWidth = width;\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.test.d.ts","sourceRoot":"","sources":["../../src/components/image.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #3455: Image component must not trigger infinite
|
|
3
|
+
* re-render loop when dimensions resolve in cmux sessions.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { Image } from "./image.js";
|
|
8
|
+
describe("Image component (#3455)", () => {
|
|
9
|
+
const theme = { fallbackColor: (s) => s };
|
|
10
|
+
test("getDimensions returns undefined before resolution", () => {
|
|
11
|
+
// Pass explicit dimensions to avoid async parsing
|
|
12
|
+
const img = new Image("base64data", "image/png", theme, {});
|
|
13
|
+
// Without explicit dims, getDimensions should be undefined until async resolve
|
|
14
|
+
// But we can't easily test async here, so verify the method exists
|
|
15
|
+
assert.equal(typeof img.getDimensions, "function");
|
|
16
|
+
});
|
|
17
|
+
test("getDimensions returns dimensions when provided at construction", () => {
|
|
18
|
+
const dims = { widthPx: 100, heightPx: 200 };
|
|
19
|
+
const img = new Image("base64data", "image/png", theme, {}, dims);
|
|
20
|
+
const result = img.getDimensions();
|
|
21
|
+
assert.deepEqual(result, dims, "Should return provided dimensions");
|
|
22
|
+
});
|
|
23
|
+
test("onDimensionsResolved callback is not called when dimensions provided", () => {
|
|
24
|
+
let callCount = 0;
|
|
25
|
+
const dims = { widthPx: 100, heightPx: 200 };
|
|
26
|
+
const img = new Image("base64data", "image/png", theme, {}, dims);
|
|
27
|
+
img.setOnDimensionsResolved(() => { callCount++; });
|
|
28
|
+
// With pre-resolved dims, the async path is skipped entirely
|
|
29
|
+
assert.equal(callCount, 0, "Callback should not fire for pre-resolved dimensions");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=image.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.test.js","sourceRoot":"","sources":["../../src/components/image.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACxC,MAAM,KAAK,GAAG,EAAE,aAAa,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;IAElD,IAAI,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC9D,kDAAkD;QAClD,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5D,+EAA+E;QAC/E,mEAAmE;QACnE,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;QAC3E,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC;QACnC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,mCAAmC,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sEAAsE,EAAE,GAAG,EAAE;QACjF,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAClE,GAAG,CAAC,uBAAuB,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,6DAA6D;QAC7D,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,EAAE,sDAAsD,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["/**\n * Regression test for #3455: Image component must not trigger infinite\n * re-render loop when dimensions resolve in cmux sessions.\n */\n\nimport { describe, test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { Image } from \"./image.js\";\n\ndescribe(\"Image component (#3455)\", () => {\n\tconst theme = { fallbackColor: (s: string) => s };\n\n\ttest(\"getDimensions returns undefined before resolution\", () => {\n\t\t// Pass explicit dimensions to avoid async parsing\n\t\tconst img = new Image(\"base64data\", \"image/png\", theme, {});\n\t\t// Without explicit dims, getDimensions should be undefined until async resolve\n\t\t// But we can't easily test async here, so verify the method exists\n\t\tassert.equal(typeof img.getDimensions, \"function\");\n\t});\n\n\ttest(\"getDimensions returns dimensions when provided at construction\", () => {\n\t\tconst dims = { widthPx: 100, heightPx: 200 };\n\t\tconst img = new Image(\"base64data\", \"image/png\", theme, {}, dims);\n\t\tconst result = img.getDimensions();\n\t\tassert.deepEqual(result, dims, \"Should return provided dimensions\");\n\t});\n\n\ttest(\"onDimensionsResolved callback is not called when dimensions provided\", () => {\n\t\tlet callCount = 0;\n\t\tconst dims = { widthPx: 100, heightPx: 200 };\n\t\tconst img = new Image(\"base64data\", \"image/png\", theme, {}, dims);\n\t\timg.setOnDimensionsResolved(() => { callCount++; });\n\t\t// With pre-resolved dims, the async path is skipped entirely\n\t\tassert.equal(callCount, 0, \"Callback should not fire for pre-resolved dimensions\");\n\t});\n});\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #3455: Image component must not trigger infinite
|
|
3
|
+
* re-render loop when dimensions resolve in cmux sessions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { Image } from "./image.js";
|
|
9
|
+
|
|
10
|
+
describe("Image component (#3455)", () => {
|
|
11
|
+
const theme = { fallbackColor: (s: string) => s };
|
|
12
|
+
|
|
13
|
+
test("getDimensions returns undefined before resolution", () => {
|
|
14
|
+
// Pass explicit dimensions to avoid async parsing
|
|
15
|
+
const img = new Image("base64data", "image/png", theme, {});
|
|
16
|
+
// Without explicit dims, getDimensions should be undefined until async resolve
|
|
17
|
+
// But we can't easily test async here, so verify the method exists
|
|
18
|
+
assert.equal(typeof img.getDimensions, "function");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("getDimensions returns dimensions when provided at construction", () => {
|
|
22
|
+
const dims = { widthPx: 100, heightPx: 200 };
|
|
23
|
+
const img = new Image("base64data", "image/png", theme, {}, dims);
|
|
24
|
+
const result = img.getDimensions();
|
|
25
|
+
assert.deepEqual(result, dims, "Should return provided dimensions");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("onDimensionsResolved callback is not called when dimensions provided", () => {
|
|
29
|
+
let callCount = 0;
|
|
30
|
+
const dims = { widthPx: 100, heightPx: 200 };
|
|
31
|
+
const img = new Image("base64data", "image/png", theme, {}, dims);
|
|
32
|
+
img.setOnDimensionsResolved(() => { callCount++; });
|
|
33
|
+
// With pre-resolved dims, the async path is skipped entirely
|
|
34
|
+
assert.equal(callCount, 0, "Callback should not fire for pre-resolved dimensions");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -72,6 +72,11 @@ export class Image implements Component {
|
|
|
72
72
|
return this.imageId;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** Get the resolved image dimensions (for caching across recreations). */
|
|
76
|
+
getDimensions(): ImageDimensions | undefined {
|
|
77
|
+
return this.dimensionsResolved ? this.dimensions : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
invalidate(): void {
|
|
76
81
|
this.cachedLines = undefined;
|
|
77
82
|
this.cachedWidth = undefined;
|
|
@@ -6,7 +6,22 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Frame, Page } from "playwright";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// sharp is an optional native dependency. Load it lazily so that the extension
|
|
11
|
+
// can still be loaded on platforms where sharp is unavailable (e.g. bunx on
|
|
12
|
+
// Raspberry Pi). constrainScreenshot falls back to returning the raw buffer
|
|
13
|
+
// when sharp is not installed, which means screenshots won't be resized but
|
|
14
|
+
// the tool remains functional.
|
|
15
|
+
let _sharp: typeof import("sharp") | null | undefined;
|
|
16
|
+
async function getSharp(): Promise<typeof import("sharp") | null> {
|
|
17
|
+
if (_sharp !== undefined) return _sharp;
|
|
18
|
+
try {
|
|
19
|
+
_sharp = (await import("sharp")).default;
|
|
20
|
+
} catch {
|
|
21
|
+
_sharp = null;
|
|
22
|
+
}
|
|
23
|
+
return _sharp;
|
|
24
|
+
}
|
|
10
25
|
import type { CompactPageState, CompactSelectorState } from "./state.js";
|
|
11
26
|
import { formatCompactStateSummary } from "./utils.js";
|
|
12
27
|
|
|
@@ -168,6 +183,9 @@ export async function constrainScreenshot(
|
|
|
168
183
|
mimeType: string,
|
|
169
184
|
quality: number,
|
|
170
185
|
): Promise<Buffer> {
|
|
186
|
+
const sharp = await getSharp();
|
|
187
|
+
if (!sharp) return buffer;
|
|
188
|
+
|
|
171
189
|
const meta = await sharp(buffer).metadata();
|
|
172
190
|
const width = meta.width;
|
|
173
191
|
const height = meta.height;
|