@selesai/code 0.5.20 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/agent-session-skill-block.test.d.ts +2 -0
- package/dist/core/agent-session-skill-block.test.d.ts.map +1 -0
- package/dist/core/agent-session-skill-block.test.js +20 -0
- package/dist/core/agent-session-skill-block.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +7 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +22 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/inline-skills.test.ts +96 -0
- package/dist/extensions/inline-skills.ts +92 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -0
- package/dist/extensions/pi-powerline-footer/tps.ts +172 -113
- package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
- package/dist/extensions/pi-subagents/README.md +602 -220
- package/dist/extensions/pi-subagents/banner.png +0 -0
- package/dist/extensions/pi-subagents/index.ts +1 -0
- package/dist/extensions/pi-subagents/install.mjs +3 -3
- package/dist/extensions/pi-subagents/package-lock.json +3405 -0
- package/dist/extensions/pi-subagents/package.json +26 -14
- package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
- package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
- package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
- package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
- package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
- package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
- package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
- package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
- package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
- package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
- package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
- package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
- package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
- package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
- package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
- package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
- package/dist/extensions/question/batch.ts +103 -0
- package/dist/extensions/question/constants.ts +1 -1
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/helpers.ts +34 -78
- package/dist/extensions/question/index.ts +412 -419
- package/dist/extensions/question/package.json +2 -1
- package/dist/extensions/question/question-list.ts +21 -14
- package/dist/extensions/question/row-layout.ts +2 -2
- package/dist/extensions/question/schemas.ts +38 -39
- package/dist/extensions/question/selection-mode.ts +5 -7
- package/dist/extensions/question/tests/batch.test.ts +75 -0
- package/dist/extensions/question/tests/helpers.test.ts +31 -156
- package/dist/extensions/question/tests/question-list.test.ts +17 -3
- package/dist/extensions/question/tests/row-layout.test.ts +4 -4
- package/dist/extensions/question/tests/schemas.test.ts +30 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tests/wizard.test.ts +99 -0
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +75 -39
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/extensions/workflow/adapter.ts +4 -76
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +10 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/dist/skills/workflow-creation/SKILL.md +2 -2
- package/docs/skills.md +8 -0
- package/docs/workflows.md +4 -4
- package/package.json +1 -1
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Owns: QuestionComponent (mode-switching + editor lifecycle + box borders),
|
|
3
|
-
// tool registration, execute() orchestration via UIProtocol.
|
|
1
|
+
// Batch-first interactive question tool.
|
|
4
2
|
|
|
5
3
|
import type { ExtensionAPI, KeybindingsManager, Theme } from "@selesai/code";
|
|
6
4
|
import {
|
|
@@ -9,563 +7,558 @@ import {
|
|
|
9
7
|
Editor,
|
|
10
8
|
type EditorTheme,
|
|
11
9
|
Key,
|
|
12
|
-
Markdown,
|
|
13
10
|
matchesKey,
|
|
14
11
|
Spacer,
|
|
15
12
|
Text,
|
|
16
13
|
type TUI,
|
|
17
14
|
truncateToWidth,
|
|
15
|
+
wrapTextWithAnsi,
|
|
18
16
|
} from "@earendil-works/pi-tui";
|
|
19
17
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
18
|
+
import { buildQuestionAnswers, prepareQuestions, saveDraftQuestion } from "./batch.ts";
|
|
19
|
+
import { BOX_BORDER_LEFT, BOX_BORDER_OVERHEAD, BOX_BORDER_RIGHT, DISABLED_SHORTCUT, QUESTION_VERSION } from "./constants.ts";
|
|
20
|
+
import { createSelectionResponse, createTextResponse, formatQuestionResponse, formatResponseSummary, oneLine, previewText, trimText } from "./helpers.ts";
|
|
23
21
|
import { QuestionList } from "./question-list.ts";
|
|
24
22
|
import { MultiSelect, SingleSelect } from "./selection-mode.ts";
|
|
25
|
-
import { resolveShortcuts } from "./shortcuts.ts";
|
|
26
23
|
import { QuestionParamsSchema } from "./schemas.ts";
|
|
27
|
-
import type {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
import type {
|
|
25
|
+
DraftQuestionState,
|
|
26
|
+
PreparedQuestion,
|
|
27
|
+
QuestionAnswer,
|
|
28
|
+
QuestionMode,
|
|
29
|
+
QuestionResponse,
|
|
30
|
+
QuestionToolResult,
|
|
31
|
+
RawQuestion,
|
|
32
|
+
} from "./types.ts";
|
|
34
33
|
|
|
35
34
|
class BoxBorderTop implements Component {
|
|
36
35
|
constructor(
|
|
37
|
-
private color: (
|
|
38
|
-
private title
|
|
39
|
-
private titleColor
|
|
36
|
+
private color: (value: string) => string,
|
|
37
|
+
private title: string,
|
|
38
|
+
private titleColor: (value: string) => string,
|
|
40
39
|
) {}
|
|
41
40
|
invalidate(): void {}
|
|
42
41
|
render(width: number): string[] {
|
|
43
42
|
const inner = Math.max(0, width - 2);
|
|
44
|
-
if (!this.title || inner < this.title.length + 4) {
|
|
45
|
-
return [this.color(`╭${"─".repeat(inner)}╮`)];
|
|
46
|
-
}
|
|
47
43
|
const label = ` ${this.title} `;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return [this.color("╭─") + titleStyle(label) + this.color("─".repeat(Math.max(0, remaining)) + "╮")];
|
|
44
|
+
if (inner < label.length + 2) return [this.color(`╭${"─".repeat(inner)}╮`)];
|
|
45
|
+
return [this.color("╭─") + this.titleColor(label) + this.color(`${"─".repeat(Math.max(0, inner - label.length - 1))}╮`)];
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
class BoxBorderBottom implements Component {
|
|
55
50
|
constructor(
|
|
56
|
-
private color: (
|
|
57
|
-
private label
|
|
58
|
-
private labelColor
|
|
51
|
+
private color: (value: string) => string,
|
|
52
|
+
private label: string,
|
|
53
|
+
private labelColor: (value: string) => string,
|
|
59
54
|
) {}
|
|
60
55
|
invalidate(): void {}
|
|
61
56
|
render(width: number): string[] {
|
|
62
57
|
const inner = Math.max(0, width - 2);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
const tag = ` ${this.label} `;
|
|
67
|
-
const leftDashes = inner - tag.length - 1;
|
|
68
|
-
const style = this.labelColor ?? this.color;
|
|
69
|
-
return [this.color("╰" + "─".repeat(Math.max(0, leftDashes))) + style(tag) + this.color("─╯")];
|
|
58
|
+
const label = ` ${this.label} `;
|
|
59
|
+
if (inner < label.length + 2) return [this.color(`╰${"─".repeat(inner)}╯`)];
|
|
60
|
+
return [this.color(`╰${"─".repeat(Math.max(0, inner - label.length - 1))}`) + this.labelColor(label) + this.color("─╯")];
|
|
70
61
|
}
|
|
71
62
|
}
|
|
72
63
|
|
|
73
|
-
function
|
|
64
|
+
function editorTheme(theme: Theme): EditorTheme {
|
|
74
65
|
return {
|
|
75
|
-
borderColor: (
|
|
66
|
+
borderColor: (value) => theme.fg("accent", value),
|
|
76
67
|
selectList: {
|
|
77
|
-
selectedPrefix: (
|
|
78
|
-
selectedText: (
|
|
79
|
-
description: (
|
|
80
|
-
scrollInfo: (
|
|
81
|
-
noMatch: (
|
|
68
|
+
selectedPrefix: (value) => theme.fg("accent", value),
|
|
69
|
+
selectedText: (value) => theme.fg("accent", value),
|
|
70
|
+
description: (value) => theme.fg("muted", value),
|
|
71
|
+
scrollInfo: (value) => theme.fg("dim", value),
|
|
72
|
+
noMatch: (value) => theme.fg("warning", value),
|
|
82
73
|
},
|
|
83
74
|
};
|
|
84
75
|
}
|
|
85
76
|
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// Question component (Container-based layout)
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
77
|
class QuestionComponent extends Container implements Component {
|
|
91
|
-
private mode: QuestionMode
|
|
92
|
-
private
|
|
93
|
-
private
|
|
94
|
-
private commentDraft = "";
|
|
95
|
-
|
|
96
|
-
private titleText: Text;
|
|
97
|
-
private questionText: Text;
|
|
98
|
-
private contextComponent?: Component;
|
|
99
|
-
private modeContainer: Container;
|
|
100
|
-
private helpText: Text;
|
|
101
|
-
|
|
78
|
+
private mode: QuestionMode;
|
|
79
|
+
private selectionValues: string[] = [];
|
|
80
|
+
private otherDraft = "";
|
|
102
81
|
private list?: QuestionList;
|
|
103
82
|
private editor?: Editor;
|
|
104
|
-
|
|
105
|
-
private
|
|
83
|
+
private editorActive = true;
|
|
84
|
+
private focusedState = false;
|
|
85
|
+
private answerText = new Text("", 1, 0);
|
|
86
|
+
private modeContainer = new Container();
|
|
87
|
+
private helpText = new Text("", 1, 0);
|
|
106
88
|
|
|
107
89
|
constructor(
|
|
108
|
-
private question:
|
|
109
|
-
private context: string | undefined,
|
|
110
|
-
private options: QuestionOption[],
|
|
111
|
-
private allowMultiple: boolean,
|
|
112
|
-
private allowFreeform: boolean,
|
|
113
|
-
private allowComment: boolean,
|
|
90
|
+
private question: PreparedQuestion,
|
|
114
91
|
private tui: TUI,
|
|
115
92
|
private theme: Theme,
|
|
116
93
|
private keybindings: KeybindingsManager,
|
|
117
|
-
private
|
|
118
|
-
|
|
94
|
+
private onCancel: () => void,
|
|
95
|
+
initialResponse?: QuestionResponse,
|
|
119
96
|
) {
|
|
120
97
|
super();
|
|
121
|
-
|
|
122
|
-
this.addChild(
|
|
123
|
-
new BoxBorderTop(
|
|
124
|
-
(s: string) => theme.fg("accent", s),
|
|
125
|
-
"question",
|
|
126
|
-
(s: string) => theme.fg("dim", theme.bold(s)),
|
|
127
|
-
),
|
|
128
|
-
);
|
|
98
|
+
this.mode = question.type === "text" ? "text" : "select";
|
|
99
|
+
this.addChild(new BoxBorderTop((value) => theme.fg("accent", value), "question", (value) => theme.fg("dim", theme.bold(value))));
|
|
129
100
|
this.addChild(new Spacer(1));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.addChild(this.titleText);
|
|
133
|
-
this.addChild(new Spacer(1));
|
|
134
|
-
|
|
135
|
-
this.questionText = new Text("", 1, 0);
|
|
136
|
-
this.addChild(this.questionText);
|
|
137
|
-
|
|
138
|
-
if (this.context) {
|
|
101
|
+
this.addChild(new Text(theme.fg("text", theme.bold(question.question)), 1, 0));
|
|
102
|
+
if (question.context) {
|
|
139
103
|
this.addChild(new Spacer(1));
|
|
140
|
-
|
|
141
|
-
if (mdTheme) {
|
|
142
|
-
this.contextComponent = new Markdown(`**Context:**\n${this.context}`, 1, 0, mdTheme);
|
|
143
|
-
} else {
|
|
144
|
-
this.contextComponent = new Text(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", this.context)}`, 1, 0);
|
|
145
|
-
}
|
|
146
|
-
this.addChild(this.contextComponent);
|
|
104
|
+
this.addChild(new Text(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", question.context)}`, 1, 0));
|
|
147
105
|
}
|
|
148
|
-
|
|
149
106
|
this.addChild(new Spacer(1));
|
|
150
|
-
|
|
151
|
-
this.
|
|
107
|
+
this.addChild(this.answerText);
|
|
108
|
+
this.addChild(new Spacer(1));
|
|
152
109
|
this.addChild(this.modeContainer);
|
|
153
|
-
|
|
154
110
|
this.addChild(new Spacer(1));
|
|
155
|
-
this.helpText = new Text("", 1, 0);
|
|
156
111
|
this.addChild(this.helpText);
|
|
157
|
-
|
|
158
112
|
this.addChild(new Spacer(1));
|
|
159
|
-
this.addChild(new BoxBorderBottom((
|
|
160
|
-
|
|
161
|
-
this.
|
|
162
|
-
this.showSelectMode();
|
|
113
|
+
this.addChild(new BoxBorderBottom((value) => theme.fg("accent", value), `v${QUESTION_VERSION}`, (value) => theme.fg("dim", value)));
|
|
114
|
+
this.restore(initialResponse);
|
|
115
|
+
this.rebuildMode();
|
|
163
116
|
}
|
|
164
117
|
|
|
165
118
|
get focused(): boolean {
|
|
166
|
-
return this.
|
|
119
|
+
return this.focusedState;
|
|
167
120
|
}
|
|
168
121
|
set focused(value: boolean) {
|
|
169
|
-
this.
|
|
170
|
-
if (this.editor
|
|
171
|
-
(this.editor as any).focused = value;
|
|
172
|
-
}
|
|
122
|
+
this.focusedState = value;
|
|
123
|
+
if (this.editor) this.editor.focused = value && this.editorActive;
|
|
173
124
|
}
|
|
174
125
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.updateStaticText();
|
|
178
|
-
this.updateHelpText();
|
|
126
|
+
get isEditing(): boolean {
|
|
127
|
+
return (this.mode === "text" || this.mode === "other") && this.editorActive;
|
|
179
128
|
}
|
|
180
129
|
|
|
181
130
|
override render(width: number): string[] {
|
|
182
131
|
const innerWidth = Math.max(1, width - BOX_BORDER_OVERHEAD);
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (index ===
|
|
188
|
-
|
|
189
|
-
return new BoxBorderBottom(borderColor, `v${QUESTION_VERSION}`, (s: string) => this.theme.fg("dim", s)).render(width)[0];
|
|
190
|
-
const padded = truncateToWidth(line, innerWidth, "", true);
|
|
191
|
-
return `${borderColor(BOX_BORDER_LEFT)}${padded}${borderColor(BOX_BORDER_RIGHT)}`;
|
|
132
|
+
const lines = super.render(innerWidth);
|
|
133
|
+
const border = (value: string) => this.theme.fg("accent", value);
|
|
134
|
+
return lines.map((line, index) => {
|
|
135
|
+
if (index === 0) return new BoxBorderTop(border, "question", (value) => this.theme.fg("dim", this.theme.bold(value))).render(width)[0]!;
|
|
136
|
+
if (index === lines.length - 1) return new BoxBorderBottom(border, `v${QUESTION_VERSION}`, (value) => this.theme.fg("dim", value)).render(width)[0]!;
|
|
137
|
+
return `${border(BOX_BORDER_LEFT)}${truncateToWidth(line, innerWidth, "", true)}${border(BOX_BORDER_RIGHT)}`;
|
|
192
138
|
});
|
|
193
139
|
}
|
|
194
140
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.
|
|
199
|
-
this.questionText.setText(theme.fg("text", theme.bold(this.question)));
|
|
200
|
-
if (this.contextComponent && this.context) {
|
|
201
|
-
const mdTheme = safeMarkdownTheme();
|
|
202
|
-
if (mdTheme && this.contextComponent instanceof Markdown) {
|
|
203
|
-
(this.contextComponent as Markdown).setText(`**Context:**\n${this.context}`);
|
|
204
|
-
} else {
|
|
205
|
-
(this.contextComponent as Text).setText(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", this.context)}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
141
|
+
override invalidate(): void {
|
|
142
|
+
super.invalidate();
|
|
143
|
+
this.updateAnswerText();
|
|
144
|
+
this.updateHelp();
|
|
208
145
|
}
|
|
209
146
|
|
|
210
|
-
private
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this.
|
|
215
|
-
? `${theme.fg("dim", this.shortcuts.commentToggle.spec)}${theme.fg("muted", " toggle context")}`
|
|
216
|
-
: null;
|
|
217
|
-
|
|
218
|
-
if (this.mode === "freeform" || this.mode === "comment") {
|
|
219
|
-
const hints = [
|
|
220
|
-
`${theme.fg("dim", kb.getKeys("tui.input.submit").join("/"))}${theme.fg("muted", this.mode === "comment" ? " submit/skip" : " submit")}`,
|
|
221
|
-
`${theme.fg("dim", kb.getKeys("tui.input.newLine").join("/"))}${theme.fg("muted", " newline")}`,
|
|
222
|
-
`${theme.fg("dim", "esc")}${theme.fg("muted", " back")}`,
|
|
223
|
-
]
|
|
224
|
-
.filter((h): h is string => !!h)
|
|
225
|
-
.join(" • ");
|
|
226
|
-
this.helpText.setText(theme.fg("dim", hints));
|
|
147
|
+
private restore(response?: QuestionResponse): void {
|
|
148
|
+
if (!response) return;
|
|
149
|
+
if (response.kind === "text") {
|
|
150
|
+
this.mode = "text";
|
|
151
|
+
this.ensureEditor().setText(response.text);
|
|
227
152
|
return;
|
|
228
153
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
`${theme.fg("dim", kb.getKeys("tui.select.confirm").join("/"))}${theme.fg("muted", " submit")}`,
|
|
236
|
-
`${theme.fg("dim", kb.getKeys("tui.select.cancel").join("/"))}${theme.fg("muted", " cancel")}`,
|
|
237
|
-
]
|
|
238
|
-
.filter((h): h is string => !!h)
|
|
239
|
-
.join(" • ");
|
|
240
|
-
this.helpText.setText(theme.fg("dim", hints));
|
|
241
|
-
} else {
|
|
242
|
-
const hints = [
|
|
243
|
-
`${theme.fg("dim", "type")}${theme.fg("muted", " filter")}`,
|
|
244
|
-
`${theme.fg("dim", kb.getKeys("tui.editor.deleteCharBackward").join("/"))}${theme.fg("muted", " erase")}`,
|
|
245
|
-
`${theme.fg("dim", "↑↓")}${theme.fg("muted", " navigate")}`,
|
|
246
|
-
commentHint,
|
|
247
|
-
`${theme.fg("dim", kb.getKeys("tui.select.confirm").join("/"))}${theme.fg("muted", " select")}`,
|
|
248
|
-
`${theme.fg("dim", "esc")}${theme.fg("muted", " clear/cancel")}`,
|
|
249
|
-
]
|
|
250
|
-
.filter((h): h is string => !!h)
|
|
251
|
-
.join(" • ");
|
|
252
|
-
this.helpText.setText(theme.fg("dim", hints));
|
|
154
|
+
this.selectionValues = [...response.values];
|
|
155
|
+
this.otherDraft = response.otherText ?? "";
|
|
156
|
+
this.ensureList().restoreSelection(response.values);
|
|
157
|
+
if (response.otherText) {
|
|
158
|
+
this.mode = "other";
|
|
159
|
+
this.ensureEditor().setText(response.otherText);
|
|
253
160
|
}
|
|
254
161
|
}
|
|
255
162
|
|
|
256
163
|
private ensureList(): QuestionList {
|
|
257
164
|
if (this.list) return this.list;
|
|
165
|
+
const multi = this.question.type === "multiselect";
|
|
258
166
|
const list = new QuestionList(
|
|
259
|
-
this.options,
|
|
260
|
-
|
|
261
|
-
this.
|
|
262
|
-
|
|
167
|
+
this.question.options,
|
|
168
|
+
multi ? MultiSelect : SingleSelect,
|
|
169
|
+
this.question.allowOther,
|
|
170
|
+
false,
|
|
263
171
|
this.theme,
|
|
264
172
|
this.keybindings,
|
|
265
|
-
|
|
173
|
+
DISABLED_SHORTCUT,
|
|
266
174
|
);
|
|
267
|
-
list.onSubmit = (
|
|
268
|
-
|
|
269
|
-
|
|
175
|
+
list.onSubmit = (values) => {
|
|
176
|
+
this.selectionValues = values;
|
|
177
|
+
if (!multi) this.otherDraft = "";
|
|
178
|
+
this.updateAnswerText();
|
|
179
|
+
this.tui.requestRender();
|
|
180
|
+
};
|
|
181
|
+
list.onEnterFreeform = () => this.showOther();
|
|
182
|
+
list.onCancel = this.onCancel;
|
|
270
183
|
this.list = list;
|
|
271
184
|
return list;
|
|
272
185
|
}
|
|
273
186
|
|
|
274
187
|
private ensureEditor(): Editor {
|
|
275
188
|
if (this.editor) return this.editor;
|
|
276
|
-
const editor = new Editor(this.tui,
|
|
277
|
-
editor.
|
|
189
|
+
const editor = new Editor(this.tui, editorTheme(this.theme));
|
|
190
|
+
editor.onChange = () => {
|
|
191
|
+
this.updateAnswerText();
|
|
192
|
+
this.tui.requestRender();
|
|
193
|
+
};
|
|
194
|
+
editor.onSubmit = (text) => {
|
|
195
|
+
editor.setText(text.trim());
|
|
196
|
+
this.updateAnswerText();
|
|
197
|
+
this.tui.requestRender();
|
|
198
|
+
};
|
|
278
199
|
this.editor = editor;
|
|
279
200
|
return editor;
|
|
280
201
|
}
|
|
281
202
|
|
|
282
|
-
private
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
203
|
+
private showOther(): void {
|
|
204
|
+
this.mode = "other";
|
|
205
|
+
this.editorActive = true;
|
|
206
|
+
const editor = this.ensureEditor();
|
|
207
|
+
editor.setText(this.otherDraft);
|
|
208
|
+
this.rebuildMode();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private showSelect(): void {
|
|
212
|
+
if (this.mode === "other") this.otherDraft = this.ensureEditor().getText();
|
|
213
|
+
this.mode = "select";
|
|
214
|
+
this.editorActive = false;
|
|
215
|
+
this.rebuildMode();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private rebuildMode(): void {
|
|
219
|
+
this.modeContainer.clear();
|
|
220
|
+
if (this.mode === "select") {
|
|
221
|
+
this.modeContainer.addChild(this.ensureList());
|
|
222
|
+
} else {
|
|
223
|
+
const editor = this.ensureEditor();
|
|
224
|
+
editor.focused = this.focusedState && this.editorActive;
|
|
225
|
+
this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(this.mode === "text" ? "Answer" : "Other answer")), 1, 0));
|
|
226
|
+
this.modeContainer.addChild(new Spacer(1));
|
|
227
|
+
this.modeContainer.addChild(editor);
|
|
228
|
+
}
|
|
229
|
+
this.updateAnswerText();
|
|
230
|
+
this.updateHelp();
|
|
231
|
+
this.invalidate();
|
|
232
|
+
this.tui.requestRender();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private updateAnswerText(): void {
|
|
236
|
+
const response = this.getResponse();
|
|
237
|
+
this.answerText.setText(
|
|
238
|
+
response
|
|
239
|
+
? `${this.theme.fg("success", "Draft:")} ${this.theme.fg("text", oneLine(formatQuestionResponse(this.question, response), 160))}`
|
|
240
|
+
: this.theme.fg("dim", "No answer yet"),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private updateHelp(): void {
|
|
245
|
+
if (this.mode === "select") {
|
|
246
|
+
const multi = this.question.type === "multiselect";
|
|
247
|
+
this.helpText.setText(
|
|
248
|
+
this.theme.fg(
|
|
249
|
+
"dim",
|
|
250
|
+
multi
|
|
251
|
+
? "↑↓ navigate • space toggle • enter save selection • Tab/→ next • Shift+Tab/← previous • esc cancel"
|
|
252
|
+
: "type filter • ↑↓ navigate • enter save choice • Tab/→ next • Shift+Tab/← previous • esc cancel",
|
|
253
|
+
),
|
|
254
|
+
);
|
|
287
255
|
return;
|
|
288
256
|
}
|
|
289
|
-
this.
|
|
257
|
+
this.helpText.setText(
|
|
258
|
+
this.theme.fg(
|
|
259
|
+
"dim",
|
|
260
|
+
`${this.keybindings.getKeys("tui.input.submit").join("/")} save • ${this.keybindings.getKeys("tui.input.newLine").join("/")} newline • Tab next • Shift+Tab previous • esc ${this.mode === "other" ? "choices" : "blur"}`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
290
263
|
}
|
|
291
264
|
|
|
292
|
-
|
|
293
|
-
if (this.
|
|
294
|
-
|
|
295
|
-
|
|
265
|
+
getResponse(): QuestionResponse | null {
|
|
266
|
+
if (this.question.type === "text") return createTextResponse(this.ensureEditor().getText());
|
|
267
|
+
const values = this.question.type === "multiselect" ? this.ensureList().getCheckedValues() : this.selectionValues;
|
|
268
|
+
const otherText = this.mode === "other" ? this.ensureEditor().getText() : this.otherDraft;
|
|
269
|
+
return createSelectionResponse(this.question.type === "select" && trimText(otherText) ? [] : values, otherText);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
handleInput(data: string): void {
|
|
273
|
+
if (this.mode === "other" && matchesKey(data, Key.escape)) {
|
|
274
|
+
this.showSelect();
|
|
296
275
|
return;
|
|
297
276
|
}
|
|
298
|
-
if (this.mode === "
|
|
299
|
-
this.
|
|
300
|
-
this.
|
|
277
|
+
if (this.mode === "text" && matchesKey(data, Key.escape) && this.editorActive) {
|
|
278
|
+
this.editorActive = false;
|
|
279
|
+
this.ensureEditor().focused = false;
|
|
280
|
+
this.updateHelp();
|
|
281
|
+
this.tui.requestRender();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if ((this.mode === "text" || this.mode === "other") && !this.editorActive) {
|
|
285
|
+
if (matchesKey(data, Key.enter)) {
|
|
286
|
+
this.editorActive = true;
|
|
287
|
+
this.ensureEditor().focused = this.focusedState;
|
|
288
|
+
this.tui.requestRender();
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
301
291
|
}
|
|
292
|
+
if (this.mode === "text" || this.mode === "other") this.ensureEditor().handleInput(data);
|
|
293
|
+
else this.ensureList().handleInput(data);
|
|
294
|
+
this.tui.requestRender();
|
|
302
295
|
}
|
|
296
|
+
}
|
|
303
297
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
298
|
+
class ReviewComponent implements Component {
|
|
299
|
+
private focusedState = false;
|
|
300
|
+
|
|
301
|
+
constructor(
|
|
302
|
+
private questions: PreparedQuestion[],
|
|
303
|
+
private states: ReadonlyMap<string, DraftQuestionState>,
|
|
304
|
+
private theme: Theme,
|
|
305
|
+
private onBack: () => void,
|
|
306
|
+
private onCancel: () => void,
|
|
307
|
+
private onSubmit: () => void,
|
|
308
|
+
) {}
|
|
309
|
+
|
|
310
|
+
get focused(): boolean {
|
|
311
|
+
return this.focusedState;
|
|
312
|
+
}
|
|
313
|
+
set focused(value: boolean) {
|
|
314
|
+
this.focusedState = value;
|
|
312
315
|
}
|
|
313
316
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
317
|
+
invalidate(): void {}
|
|
318
|
+
|
|
319
|
+
render(width: number): string[] {
|
|
320
|
+
const safeWidth = Math.max(12, width);
|
|
321
|
+
const lines: string[] = [];
|
|
322
|
+
for (const question of this.questions) {
|
|
323
|
+
const state = this.states.get(question.id);
|
|
324
|
+
const status = state?.status ?? "unanswered";
|
|
325
|
+
const marker = status === "answered" ? this.theme.fg("success", "✓") : status === "skipped" ? this.theme.fg("warning", "−") : this.theme.fg("dim", "○");
|
|
326
|
+
lines.push(truncateToWidth(`${marker} ${this.theme.fg("accent", this.theme.bold(question.label))} · ${this.theme.fg(status === "answered" ? "success" : status === "skipped" ? "warning" : "dim", status)}`, safeWidth, ""));
|
|
327
|
+
lines.push(...wrapTextWithAnsi(this.theme.fg("dim", question.question), safeWidth));
|
|
328
|
+
if (state?.status === "answered" && state.response) {
|
|
329
|
+
const preview = previewText(formatQuestionResponse(question, state.response));
|
|
330
|
+
lines.push(...wrapTextWithAnsi(`${this.theme.fg("dim", "Answer:")} ${this.theme.fg("text", preview)}`, safeWidth));
|
|
331
|
+
}
|
|
332
|
+
lines.push("");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const bodyBudget = Math.max(7, (process.stdout.rows || 24) - 9);
|
|
336
|
+
let body = lines;
|
|
337
|
+
if (lines.length > bodyBudget) {
|
|
338
|
+
body = lines.slice(-bodyBudget);
|
|
339
|
+
body[0] = this.theme.fg("dim", "↑ earlier answers");
|
|
340
|
+
}
|
|
341
|
+
const submit = this.theme.bg("selectedBg", this.theme.fg("text", " Submit "));
|
|
342
|
+
return [...body, truncateToWidth(submit, safeWidth, "")];
|
|
326
343
|
}
|
|
327
344
|
|
|
328
|
-
|
|
329
|
-
this.
|
|
330
|
-
this.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
handleInput(data: string): void {
|
|
346
|
+
if (matchesKey(data, Key.enter)) this.onSubmit();
|
|
347
|
+
else if (matchesKey(data, Key.left)) this.onBack();
|
|
348
|
+
else if (matchesKey(data, Key.escape)) this.onCancel();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
class BatchQuestionComponent extends Container implements Component {
|
|
353
|
+
private currentPage = 0;
|
|
354
|
+
private states = new Map<string, DraftQuestionState>();
|
|
355
|
+
private questionComponent?: QuestionComponent;
|
|
356
|
+
private reviewComponent?: ReviewComponent;
|
|
357
|
+
private focusedState = false;
|
|
358
|
+
private header = new Text("", 1, 0);
|
|
359
|
+
private content = new Container();
|
|
360
|
+
private legend = new Text("", 1, 0);
|
|
361
|
+
|
|
362
|
+
constructor(
|
|
363
|
+
private questions: PreparedQuestion[],
|
|
364
|
+
private tui: TUI,
|
|
365
|
+
private theme: Theme,
|
|
366
|
+
private keybindings: KeybindingsManager,
|
|
367
|
+
private onDone: (result: QuestionToolResult) => void,
|
|
368
|
+
) {
|
|
369
|
+
super();
|
|
370
|
+
this.addChild(this.header);
|
|
371
|
+
this.addChild(new Spacer(1));
|
|
372
|
+
this.addChild(this.content);
|
|
373
|
+
this.addChild(new Spacer(1));
|
|
374
|
+
this.addChild(this.legend);
|
|
375
|
+
this.showPage();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
get focused(): boolean {
|
|
379
|
+
return this.focusedState;
|
|
380
|
+
}
|
|
381
|
+
set focused(value: boolean) {
|
|
382
|
+
this.focusedState = value;
|
|
383
|
+
if (this.questionComponent) this.questionComponent.focused = value;
|
|
384
|
+
if (this.reviewComponent) this.reviewComponent.focused = value;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
override invalidate(): void {
|
|
388
|
+
super.invalidate();
|
|
389
|
+
this.updateChrome();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private get isReviewPage(): boolean {
|
|
393
|
+
return this.currentPage === this.questions.length;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private cancel = (): void => this.onDone({ status: "cancelled", reason: "user" });
|
|
397
|
+
|
|
398
|
+
private updateChrome(): void {
|
|
399
|
+
if (this.isReviewPage) {
|
|
400
|
+
const answered = [...this.states.values()].filter((state) => state.status === "answered").length;
|
|
401
|
+
const skipped = [...this.states.values()].filter((state) => state.status === "skipped").length;
|
|
402
|
+
this.header.setText(this.theme.fg("accent", this.theme.bold(`Review · ${answered} answered · ${skipped} skipped · ${this.questions.length - this.states.size} unanswered`)));
|
|
403
|
+
this.legend.setText(this.theme.fg("dim", "Enter submit • ← edit answers • Esc cancel"));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const question = this.questions[this.currentPage]!;
|
|
407
|
+
const status = this.states.get(question.id)?.status ?? "unanswered";
|
|
408
|
+
this.header.setText(this.theme.fg("accent", this.theme.bold(`${question.label} · Question ${this.currentPage + 1} of ${this.questions.length} · ${status}`)));
|
|
409
|
+
this.legend.setText(this.theme.fg("dim", "Tab/→ next • Shift+Tab/← previous • blank Next skips • answers commit only on final Submit"));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private showPage(): void {
|
|
413
|
+
this.content.clear();
|
|
414
|
+
this.questionComponent = undefined;
|
|
415
|
+
this.reviewComponent = undefined;
|
|
416
|
+
if (this.isReviewPage) {
|
|
417
|
+
this.reviewComponent = new ReviewComponent(
|
|
418
|
+
this.questions,
|
|
419
|
+
this.states,
|
|
420
|
+
this.theme,
|
|
421
|
+
() => {
|
|
422
|
+
this.currentPage = this.questions.length - 1;
|
|
423
|
+
this.showPage();
|
|
424
|
+
},
|
|
425
|
+
this.cancel,
|
|
426
|
+
() => this.onDone({ status: "submitted", answers: buildQuestionAnswers(this.questions, this.states) }),
|
|
427
|
+
);
|
|
428
|
+
this.reviewComponent.focused = this.focusedState;
|
|
429
|
+
this.content.addChild(this.reviewComponent);
|
|
430
|
+
} else {
|
|
431
|
+
const question = this.questions[this.currentPage]!;
|
|
432
|
+
const state = this.states.get(question.id);
|
|
433
|
+
this.questionComponent = new QuestionComponent(question, this.tui, this.theme, this.keybindings, this.cancel, state?.response);
|
|
434
|
+
this.questionComponent.focused = this.focusedState;
|
|
435
|
+
this.content.addChild(this.questionComponent);
|
|
436
|
+
}
|
|
437
|
+
this.updateChrome();
|
|
340
438
|
this.invalidate();
|
|
341
439
|
this.tui.requestRender();
|
|
342
440
|
}
|
|
343
441
|
|
|
442
|
+
private saveCurrent(markBlankSkipped: boolean): void {
|
|
443
|
+
if (this.isReviewPage || !this.questionComponent) return;
|
|
444
|
+
const question = this.questions[this.currentPage]!;
|
|
445
|
+
const response = this.questionComponent.getResponse();
|
|
446
|
+
saveDraftQuestion(this.states, question.id, response, markBlankSkipped);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private goNext(): void {
|
|
450
|
+
this.saveCurrent(true);
|
|
451
|
+
if (this.currentPage < this.questions.length) this.currentPage++;
|
|
452
|
+
this.showPage();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private goPrevious(): void {
|
|
456
|
+
this.saveCurrent(false);
|
|
457
|
+
if (this.currentPage > 0) this.currentPage--;
|
|
458
|
+
this.showPage();
|
|
459
|
+
}
|
|
460
|
+
|
|
344
461
|
handleInput(data: string): void {
|
|
345
|
-
if (this.
|
|
346
|
-
|
|
347
|
-
this.showSelectMode();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
351
|
-
this.onDone(null);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
this.ensureEditor().handleInput(data);
|
|
355
|
-
this.tui.requestRender();
|
|
462
|
+
if (this.isReviewPage) {
|
|
463
|
+
this.reviewComponent?.handleInput(data);
|
|
356
464
|
return;
|
|
357
465
|
}
|
|
358
|
-
|
|
359
|
-
|
|
466
|
+
if (matchesKey(data, Key.tab) || (!this.questionComponent?.isEditing && matchesKey(data, Key.right))) {
|
|
467
|
+
this.goNext();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (matchesKey(data, Key.shift("tab")) || (!this.questionComponent?.isEditing && matchesKey(data, Key.left))) {
|
|
471
|
+
this.goPrevious();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (!this.questionComponent?.isEditing && matchesKey(data, Key.escape) && this.questions[this.currentPage]?.type === "text") {
|
|
475
|
+
this.cancel();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this.questionComponent?.handleInput(data);
|
|
360
479
|
}
|
|
361
480
|
}
|
|
362
481
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
482
|
+
function answerText(answer: QuestionAnswer): string {
|
|
483
|
+
if (answer.status !== "answered") return answer.status;
|
|
484
|
+
return formatResponseSummary(answer.response);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function rawQuestionLabel(question: RawQuestion | undefined, answer: QuestionAnswer): string {
|
|
488
|
+
if (!question || answer.status !== "answered" || answer.response.kind === "text") return answerText(answer);
|
|
489
|
+
const labels = answer.response.values.map((value) => question.options.find((option) => option.value === value)?.label ?? value);
|
|
490
|
+
if (answer.response.otherText) labels.push(`Other: ${answer.response.otherText}`);
|
|
491
|
+
return labels.join(", ");
|
|
492
|
+
}
|
|
366
493
|
|
|
367
494
|
export default function questionExtension(pi: ExtensionAPI) {
|
|
368
495
|
pi.registerTool({
|
|
369
496
|
name: "question",
|
|
370
497
|
label: "Question",
|
|
371
498
|
description:
|
|
372
|
-
"Ask
|
|
373
|
-
promptSnippet: "Ask the user
|
|
499
|
+
"Ask one or more typed questions in a terminal wizard. Supports select, multiselect, text, stable option values, Other answers, and partial atomic submission.",
|
|
500
|
+
promptSnippet: "Ask the user one or more typed questions when a decision is required.",
|
|
374
501
|
promptGuidelines: [
|
|
375
|
-
"Use question only
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"Use
|
|
379
|
-
"
|
|
380
|
-
"After the user answers, continue the task immediately using that answer; do not stop and ask the user to type continue unless the task is complete.",
|
|
381
|
-
"Do not use question for information you can infer, inspect, or compute with other tools.",
|
|
502
|
+
"Use question only for decisions the user must make; inspect facts yourself.",
|
|
503
|
+
"Send one questions array containing independent decisions that can be answered together.",
|
|
504
|
+
"Use stable option values separate from human-readable labels.",
|
|
505
|
+
"Use text questions only when known choices cannot represent the answer.",
|
|
506
|
+
"After question returns, continue immediately using submitted answers; skipped and unanswered are distinct states.",
|
|
382
507
|
],
|
|
383
|
-
// Block other tool calls in the same turn until the user answers, so the
|
|
384
|
-
// model can't batch question with side-effecting tools and run them first.
|
|
385
508
|
executionMode: "sequential",
|
|
386
509
|
parameters: QuestionParamsSchema,
|
|
387
510
|
|
|
388
511
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const rawContext = (params.context as string | undefined)?.trim() || (params.title as string | undefined)?.trim() || undefined;
|
|
399
|
-
const allowMultiple = params.allowMultiple ?? false;
|
|
400
|
-
const allowFreeform = params.allowFreeform ?? !((params.options ?? []).length > 0);
|
|
401
|
-
const allowComment = params.allowComment ?? false;
|
|
402
|
-
const timeout = params.timeout as number | undefined;
|
|
403
|
-
|
|
404
|
-
const shortcuts: ResolvedShortcuts = resolveShortcuts(
|
|
405
|
-
params.commentToggleKey as string | null | undefined,
|
|
406
|
-
process.env.PI_QUESTION_COMMENT_TOGGLE_KEY,
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
const optionsFormatError = getOptionsFormatError(params.options as RawOption[] | undefined);
|
|
410
|
-
if (optionsFormatError) {
|
|
411
|
-
return {
|
|
412
|
-
content: [{ type: "text", text: optionsFormatError }],
|
|
413
|
-
isError: true,
|
|
414
|
-
details: { question: rawQuestion, context: rawContext, options: [], response: null, cancelled: true } satisfies QuestionDetails,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const options = normalizeOptions(params.options as RawOption[] | undefined);
|
|
419
|
-
const normalizedContext = rawContext || undefined;
|
|
420
|
-
const detailsBase = { question: rawQuestion, context: normalizedContext, options };
|
|
421
|
-
|
|
422
|
-
const protocol: UIProtocol = createTUIProtocol(ctx);
|
|
423
|
-
|
|
424
|
-
if (!protocol.hasUI) {
|
|
425
|
-
const optionText = options.length > 0 ? `\n\nOptions:\n${formatOptionsForMessage(options)}` : "";
|
|
426
|
-
const freeformHint = allowFreeform ? "\n\nYou can also answer freely." : "";
|
|
427
|
-
const commentHint = allowComment ? "\n\nAfter choosing, you may add an optional comment." : "";
|
|
428
|
-
const contextText = normalizedContext ? `\n\nContext:\n${normalizedContext}` : "";
|
|
429
|
-
return {
|
|
430
|
-
content: [
|
|
431
|
-
{ type: "text", text: `Question requires interactive mode. Please answer:\n\n${rawQuestion}${contextText}${optionText}${freeformHint}${commentHint}` },
|
|
432
|
-
],
|
|
433
|
-
isError: true,
|
|
434
|
-
details: { ...detailsBase, response: null, cancelled: true } satisfies QuestionDetails,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (options.length === 0 && !allowFreeform) {
|
|
439
|
-
return {
|
|
440
|
-
content: [{ type: "text", text: "Question options are empty and allowFreeform is false." }],
|
|
441
|
-
isError: true,
|
|
442
|
-
details: { ...detailsBase, response: null, cancelled: true } satisfies QuestionDetails,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
onUpdate?.({
|
|
447
|
-
content: [{ type: "text", text: "Waiting for user input..." }],
|
|
448
|
-
details: { ...detailsBase, response: null, cancelled: false } satisfies QuestionDetails,
|
|
512
|
+
const prepared = prepareQuestions(params.questions);
|
|
513
|
+
if ("error" in prepared) throw new Error(prepared.error);
|
|
514
|
+
if (ctx.mode !== "tui") throw new Error("Question requires terminal TUI mode.");
|
|
515
|
+
if (signal?.aborted) return { content: [{ type: "text", text: "Question cancelled" }], details: { status: "cancelled", reason: "user" } satisfies QuestionToolResult };
|
|
516
|
+
|
|
517
|
+
onUpdate?.({ content: [{ type: "text", text: `Waiting for answers to ${prepared.questions.length} question${prepared.questions.length === 1 ? "" : "s"}...` }] });
|
|
518
|
+
const result = await ctx.ui.custom<QuestionToolResult>((tui, theme, keybindings, done) => {
|
|
519
|
+
if (signal) signal.addEventListener("abort", () => done({ status: "cancelled", reason: "user" }), { once: true });
|
|
520
|
+
return new BatchQuestionComponent(prepared.questions, tui, theme, keybindings, done);
|
|
449
521
|
});
|
|
522
|
+
if (!result) throw new Error("Question TUI did not return a result.");
|
|
450
523
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const customFactory: CustomFactory = (
|
|
455
|
-
tui: TUI,
|
|
456
|
-
theme: Theme,
|
|
457
|
-
keybindings: KeybindingsManager,
|
|
458
|
-
done: (result: QuestionResponse | null) => void,
|
|
459
|
-
) => {
|
|
460
|
-
if (signal) {
|
|
461
|
-
const onAbort = () => done(null);
|
|
462
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
463
|
-
}
|
|
464
|
-
if (timeout && timeout > 0) setTimeout(() => done(null), timeout);
|
|
465
|
-
|
|
466
|
-
return new QuestionComponent(
|
|
467
|
-
rawQuestion,
|
|
468
|
-
normalizedContext,
|
|
469
|
-
options,
|
|
470
|
-
allowMultiple,
|
|
471
|
-
allowFreeform,
|
|
472
|
-
allowComment,
|
|
473
|
-
tui,
|
|
474
|
-
theme,
|
|
475
|
-
keybindings,
|
|
476
|
-
shortcuts,
|
|
477
|
-
done,
|
|
478
|
-
);
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const customResult = await protocol.custom(customFactory);
|
|
482
|
-
|
|
483
|
-
if (customResult !== undefined) {
|
|
484
|
-
result = customResult;
|
|
485
|
-
} else {
|
|
486
|
-
// RPC/headless: custom() returns undefined — degrade to dialog protocol.
|
|
487
|
-
result = await DialogFallback.ask(
|
|
488
|
-
{ question: rawQuestion, context: normalizedContext, options, allowMultiple, allowFreeform, allowComment, shortcuts, timeout },
|
|
489
|
-
protocol,
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
} catch (error) {
|
|
493
|
-
const message = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
|
|
494
|
-
return {
|
|
495
|
-
content: [{ type: "text", text: `Question tool failed: ${message}` }],
|
|
496
|
-
isError: true,
|
|
497
|
-
details: { error: message } as unknown as QuestionDetails,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (result === null) {
|
|
502
|
-
pi.events.emit("question:cancelled", { question: rawQuestion, context: normalizedContext, options });
|
|
503
|
-
return {
|
|
504
|
-
content: [{ type: "text", text: "User cancelled the question" }],
|
|
505
|
-
details: { ...detailsBase, response: null, cancelled: true } satisfies QuestionDetails,
|
|
506
|
-
};
|
|
524
|
+
if (result.status === "cancelled") {
|
|
525
|
+
pi.events.emit("question:cancelled", { reason: result.reason });
|
|
526
|
+
return { content: [{ type: "text", text: "User cancelled the question batch" }], details: result };
|
|
507
527
|
}
|
|
508
|
-
|
|
509
|
-
pi.events.emit("question:answered", { question: rawQuestion, context: normalizedContext, response: result });
|
|
528
|
+
pi.events.emit("question:submitted", { answers: result.answers });
|
|
510
529
|
return {
|
|
511
|
-
content: [{ type: "text", text:
|
|
512
|
-
details:
|
|
530
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
531
|
+
details: result,
|
|
513
532
|
};
|
|
514
533
|
},
|
|
515
534
|
|
|
516
|
-
renderCall(args, theme
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const title = args.context ? `${args.context}: ` : args.title ? `${args.title}: ` : "";
|
|
520
|
-
const optionCount = rawOptions.length;
|
|
521
|
-
const flags: string[] = [];
|
|
522
|
-
if (args.allowMultiple) flags.push("multi");
|
|
523
|
-
if (args.allowComment) flags.push("comment");
|
|
524
|
-
const suffix = optionCount
|
|
525
|
-
? theme.fg("dim", ` · ${optionCount} option${optionCount === 1 ? "" : "s"}${flags.length ? ` · ${flags.join("/")}` : ""}`)
|
|
526
|
-
: theme.fg("dim", ` · freeform${flags.length ? ` · ${flags.join("/")}` : ""}`);
|
|
535
|
+
renderCall(args, theme) {
|
|
536
|
+
const questions = Array.isArray(args.questions) ? (args.questions as RawQuestion[]) : [];
|
|
537
|
+
const labels = questions.map((question, index) => question.label || question.id || `Q${index + 1}`).join(", ");
|
|
527
538
|
return new Text(
|
|
528
|
-
`${theme.fg("toolTitle", theme.bold("QUESTION"))} ${theme.fg("accent", theme.bold(
|
|
539
|
+
`${theme.fg("toolTitle", theme.bold("QUESTION"))} ${theme.fg("accent", theme.bold(`${questions.length} question${questions.length === 1 ? "" : "s"}`))}${labels ? theme.fg("dim", ` · ${oneLine(labels, 100)}`) : ""}`,
|
|
529
540
|
0,
|
|
530
541
|
0,
|
|
531
542
|
);
|
|
532
543
|
},
|
|
533
544
|
|
|
534
|
-
renderResult(result, options, theme,
|
|
535
|
-
const details = result.details as (QuestionDetails & { error?: string }) | undefined;
|
|
536
|
-
|
|
537
|
-
if (details?.error) return new Text(theme.fg("error", details.error), 0, 0);
|
|
538
|
-
|
|
545
|
+
renderResult(result, options, theme, context) {
|
|
539
546
|
if (options.isPartial) {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
?.filter((part: { type?: string; text?: string }) => part?.type === "text")
|
|
543
|
-
.map((part: { text?: string }) => part.text ?? "")
|
|
544
|
-
.join("\n")
|
|
545
|
-
.trim() || "Waiting for user input...";
|
|
546
|
-
return new Text(theme.fg("muted", waitingText), 0, 0);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (!details || details.cancelled || !details.response) {
|
|
550
|
-
return new Text(theme.fg("warning", "question cancelled"), 0, 0);
|
|
547
|
+
const text = result.content.find((part: { type?: string }) => part.type === "text") as { text?: string } | undefined;
|
|
548
|
+
return new Text(theme.fg("muted", text?.text ?? "Waiting for user input..."), 0, 0);
|
|
551
549
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const desc = opt.description ? ` — ${opt.description}` : "";
|
|
565
|
-
const marker = selected.has(opt.label) ? theme.fg("success", "●") : theme.fg("dim", "○");
|
|
566
|
-
text += `\n ${marker} ${theme.fg("dim", opt.label)}${theme.fg("dim", desc)}`;
|
|
567
|
-
}
|
|
568
|
-
if (response.comment) text += `\n${theme.fg("dim", "Comment:")} ${theme.fg("dim", response.comment)}`;
|
|
550
|
+
const details = result.details as QuestionToolResult | undefined;
|
|
551
|
+
if (!details || details.status === "cancelled") return new Text(theme.fg("warning", "question cancelled"), 0, 0);
|
|
552
|
+
const answered = details.answers.filter((answer) => answer.status === "answered").length;
|
|
553
|
+
const skipped = details.answers.filter((answer) => answer.status === "skipped").length;
|
|
554
|
+
const unanswered = details.answers.length - answered - skipped;
|
|
555
|
+
let text = `${theme.fg("success", "submitted")} ${theme.fg("dim", `· ${answered} answered · ${skipped} skipped · ${unanswered} unanswered`)}`;
|
|
556
|
+
if (options.expanded) {
|
|
557
|
+
const questions = (((context as { args?: { questions?: RawQuestion[] } }).args?.questions ?? []) as RawQuestion[]);
|
|
558
|
+
for (const answer of details.answers) {
|
|
559
|
+
const question = questions.find((candidate, index) => (candidate.id?.trim() || `q${index + 1}`) === answer.id);
|
|
560
|
+
const marker = answer.status === "answered" ? theme.fg("success", "✓") : answer.status === "skipped" ? theme.fg("warning", "−") : theme.fg("dim", "○");
|
|
561
|
+
text += `\n${marker} ${theme.fg("accent", answer.id)}: ${theme.fg(answer.status === "answered" ? "text" : "dim", oneLine(rawQuestionLabel(question, answer), 200))}`;
|
|
569
562
|
}
|
|
570
563
|
}
|
|
571
564
|
return new Text(text, 0, 0);
|