@selesai/code 0.5.19 → 0.5.21
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/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
- package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
- 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 +39 -0
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/index.ts +237 -7
- package/dist/extensions/question/schemas.ts +28 -15
- package/dist/extensions/question/tests/batch.test.ts +26 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +37 -0
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/package.json +2 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isCancelled,
|
|
9
9
|
parseDialogSelections,
|
|
10
10
|
} from "./helpers.ts";
|
|
11
|
-
import type { QuestionOption, QuestionResponse, ResolvedQuestionParams } from "./types.ts";
|
|
11
|
+
import type { BatchAnswer, BatchedQuestion, QuestionOption, QuestionResponse, ResolvedQuestionParams } from "./types.ts";
|
|
12
12
|
import type { FallbackProtocol, UIProtocol } from "./ui-protocol.ts";
|
|
13
13
|
|
|
14
14
|
const FREEFORM_DIALOG_OPTION = "✏️ Type custom response...";
|
|
@@ -58,6 +58,28 @@ async function askViaDialogs(
|
|
|
58
58
|
return createSelectionResponse([selected], comment);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export async function askBatchViaDialogs(questions: BatchedQuestion[], protocol: UIProtocol, timeout?: number): Promise<BatchAnswer[] | null> {
|
|
62
|
+
const deadline = timeout && timeout > 0 ? Date.now() + timeout : undefined;
|
|
63
|
+
const answers: BatchAnswer[] = [];
|
|
64
|
+
for (const question of questions) {
|
|
65
|
+
const remaining = deadline === undefined ? undefined : deadline - Date.now();
|
|
66
|
+
if (remaining !== undefined && remaining <= 0) return null;
|
|
67
|
+
const response = await askViaDialogs(
|
|
68
|
+
protocol,
|
|
69
|
+
question.question,
|
|
70
|
+
question.context,
|
|
71
|
+
question.options,
|
|
72
|
+
question.allowMultiple,
|
|
73
|
+
question.allowFreeform,
|
|
74
|
+
question.allowComment,
|
|
75
|
+
remaining,
|
|
76
|
+
);
|
|
77
|
+
if (response === null) return null;
|
|
78
|
+
answers.push({ id: question.id, question: question.question, response });
|
|
79
|
+
}
|
|
80
|
+
return answers;
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
export const DialogFallback: FallbackProtocol = {
|
|
62
84
|
async ask(params, protocol) {
|
|
63
85
|
return askViaDialogs(
|
|
@@ -17,14 +17,15 @@ import {
|
|
|
17
17
|
truncateToWidth,
|
|
18
18
|
} from "@earendil-works/pi-tui";
|
|
19
19
|
|
|
20
|
+
import { prepareBatchQuestions } from "./batch.ts";
|
|
20
21
|
import { BOX_BORDER_LEFT, BOX_BORDER_OVERHEAD, BOX_BORDER_RIGHT, QUESTION_VERSION } from "./constants.ts";
|
|
21
|
-
import { DialogFallback } from "./dialog-adapter.ts";
|
|
22
|
+
import { askBatchViaDialogs, DialogFallback } from "./dialog-adapter.ts";
|
|
22
23
|
import { createFreeformResponse, createSelectionResponse, formatOptionsForMessage, formatResponseSummary, getOptionsFormatError, normalizeOptions, oneLine, safeMarkdownTheme } from "./helpers.ts";
|
|
23
24
|
import { QuestionList } from "./question-list.ts";
|
|
24
25
|
import { MultiSelect, SingleSelect } from "./selection-mode.ts";
|
|
25
26
|
import { resolveShortcuts } from "./shortcuts.ts";
|
|
26
27
|
import { QuestionParamsSchema } from "./schemas.ts";
|
|
27
|
-
import type { QuestionDetails, QuestionMode, QuestionOption, QuestionResponse, RawOption, ResolvedShortcuts } from "./types.ts";
|
|
28
|
+
import type { BatchAnswer, BatchQuestionDetails, BatchedQuestion, QuestionDetails, QuestionMode, QuestionOption, QuestionResponse, RawOption, ResolvedShortcuts } from "./types.ts";
|
|
28
29
|
import { createTUIProtocol } from "./tui-adapter.ts";
|
|
29
30
|
import type { CustomFactory, UIProtocol } from "./ui-protocol.ts";
|
|
30
31
|
|
|
@@ -165,6 +166,10 @@ class QuestionComponent extends Container implements Component {
|
|
|
165
166
|
get focused(): boolean {
|
|
166
167
|
return this._focused;
|
|
167
168
|
}
|
|
169
|
+
|
|
170
|
+
get isEditing(): boolean {
|
|
171
|
+
return this.mode === "freeform" || this.mode === "comment";
|
|
172
|
+
}
|
|
168
173
|
set focused(value: boolean) {
|
|
169
174
|
this._focused = value;
|
|
170
175
|
if (this.editor && (this.mode === "freeform" || this.mode === "comment")) {
|
|
@@ -360,6 +365,171 @@ class QuestionComponent extends Container implements Component {
|
|
|
360
365
|
}
|
|
361
366
|
}
|
|
362
367
|
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Batched-question component
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
class BatchQuestionComponent extends Container implements Component {
|
|
373
|
+
private currentPage = 0;
|
|
374
|
+
private readonly answers = new Map<string, QuestionResponse>();
|
|
375
|
+
private questionComponent?: QuestionComponent;
|
|
376
|
+
private focusedState = false;
|
|
377
|
+
private header = new Text("", 1, 0);
|
|
378
|
+
private content = new Container();
|
|
379
|
+
private legend = new Text("", 1, 0);
|
|
380
|
+
|
|
381
|
+
constructor(
|
|
382
|
+
private questions: BatchedQuestion[],
|
|
383
|
+
private tui: TUI,
|
|
384
|
+
private theme: Theme,
|
|
385
|
+
private keybindings: KeybindingsManager,
|
|
386
|
+
private onDone: (answers: BatchAnswer[] | null) => void,
|
|
387
|
+
) {
|
|
388
|
+
super();
|
|
389
|
+
this.addChild(this.header);
|
|
390
|
+
this.addChild(new Spacer(1));
|
|
391
|
+
this.addChild(this.content);
|
|
392
|
+
this.addChild(new Spacer(1));
|
|
393
|
+
this.addChild(this.legend);
|
|
394
|
+
this.showPage();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get focused(): boolean {
|
|
398
|
+
return this.focusedState;
|
|
399
|
+
}
|
|
400
|
+
set focused(value: boolean) {
|
|
401
|
+
this.focusedState = value;
|
|
402
|
+
if (this.questionComponent) this.questionComponent.focused = value;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
override invalidate(): void {
|
|
406
|
+
super.invalidate();
|
|
407
|
+
this.updateChrome();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private get isReviewPage(): boolean {
|
|
411
|
+
return this.currentPage === this.questions.length;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private allAnswered(): boolean {
|
|
415
|
+
return this.questions.every((question) => this.answers.has(question.id));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private updateChrome(): void {
|
|
419
|
+
const answered = this.answers.size;
|
|
420
|
+
if (this.isReviewPage) {
|
|
421
|
+
this.header.setText(this.theme.fg("accent", this.theme.bold(`Review answers · ${answered}/${this.questions.length} answered`)));
|
|
422
|
+
this.legend.setText(
|
|
423
|
+
this.theme.fg(
|
|
424
|
+
"dim",
|
|
425
|
+
this.allAnswered()
|
|
426
|
+
? "Shift+Tab/← previous • Enter submit all answers • Esc cancel"
|
|
427
|
+
: "Shift+Tab/← previous • Answer every question before submitting • Esc cancel",
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const question = this.questions[this.currentPage];
|
|
433
|
+
this.header.setText(
|
|
434
|
+
this.theme.fg(
|
|
435
|
+
"accent",
|
|
436
|
+
this.theme.bold(`${question.label} · ${this.currentPage + 1}/${this.questions.length}${this.answers.has(question.id) ? " · answered" : ""}`),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
this.legend.setText(this.theme.fg("dim", "Tab next • Shift+Tab previous • ←→ navigate outside text entry • Answers save when selected"));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private showPage(): void {
|
|
443
|
+
this.content.clear();
|
|
444
|
+
this.questionComponent = undefined;
|
|
445
|
+
this.updateChrome();
|
|
446
|
+
|
|
447
|
+
if (this.isReviewPage) {
|
|
448
|
+
this.content.addChild(new Text(this.theme.fg("accent", this.theme.bold("Ready to submit")), 1, 0));
|
|
449
|
+
this.content.addChild(new Spacer(1));
|
|
450
|
+
for (const question of this.questions) {
|
|
451
|
+
const response = this.answers.get(question.id);
|
|
452
|
+
const marker = response ? this.theme.fg("success", "✓") : this.theme.fg("warning", "○");
|
|
453
|
+
const answer = response ? formatResponseSummary(response) : "Unanswered";
|
|
454
|
+
this.content.addChild(new Text(`${marker} ${this.theme.fg("accent", question.label)} ${this.theme.fg("dim", "—")} ${this.theme.fg(response ? "text" : "warning", answer)}`, 1, 0));
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
const question = this.questions[this.currentPage];
|
|
458
|
+
this.questionComponent = new QuestionComponent(
|
|
459
|
+
question.question,
|
|
460
|
+
question.context,
|
|
461
|
+
question.options,
|
|
462
|
+
question.allowMultiple,
|
|
463
|
+
question.allowFreeform,
|
|
464
|
+
question.allowComment,
|
|
465
|
+
this.tui,
|
|
466
|
+
this.theme,
|
|
467
|
+
this.keybindings,
|
|
468
|
+
question.shortcuts,
|
|
469
|
+
(response) => this.saveAnswer(question, response),
|
|
470
|
+
);
|
|
471
|
+
this.questionComponent.focused = this.focusedState;
|
|
472
|
+
this.content.addChild(this.questionComponent);
|
|
473
|
+
}
|
|
474
|
+
this.invalidate();
|
|
475
|
+
this.tui.requestRender();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private saveAnswer(question: BatchedQuestion, response: QuestionResponse | null): void {
|
|
479
|
+
if (response === null) {
|
|
480
|
+
this.onDone(null);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
this.answers.set(question.id, response);
|
|
484
|
+
this.goNext();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private goNext(): void {
|
|
488
|
+
if (this.currentPage < this.questions.length) this.currentPage++;
|
|
489
|
+
this.showPage();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private goPrevious(): void {
|
|
493
|
+
if (this.currentPage > 0) this.currentPage--;
|
|
494
|
+
this.showPage();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
handleInput(data: string): void {
|
|
498
|
+
if (this.isReviewPage) {
|
|
499
|
+
if (matchesKey(data, Key.enter) && this.allAnswered()) {
|
|
500
|
+
this.onDone(this.questions.map((question) => ({ id: question.id, question: question.question, response: this.answers.get(question.id)! })));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (matchesKey(data, Key.escape) || this.keybindings.matches(data, "tui.select.cancel")) {
|
|
504
|
+
this.onDone(null);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) this.goPrevious();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (matchesKey(data, Key.tab)) {
|
|
512
|
+
this.goNext();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (matchesKey(data, Key.shift("tab"))) {
|
|
516
|
+
this.goPrevious();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!this.questionComponent?.isEditing) {
|
|
520
|
+
if (matchesKey(data, Key.right)) {
|
|
521
|
+
this.goNext();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (matchesKey(data, Key.left)) {
|
|
525
|
+
this.goPrevious();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
this.questionComponent?.handleInput(data);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
363
533
|
// ---------------------------------------------------------------------------
|
|
364
534
|
// Tool registration
|
|
365
535
|
// ---------------------------------------------------------------------------
|
|
@@ -369,7 +539,7 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
369
539
|
name: "question",
|
|
370
540
|
label: "Question",
|
|
371
541
|
description:
|
|
372
|
-
"Ask the user
|
|
542
|
+
"Ask the user one question or a paged batch through the Pi UI and return the answer(s). Supports multiple-choice options, multi-select, freeform answers, optional context, comments, and a final batch review/submit page. Use when you need user input to proceed.",
|
|
373
543
|
promptSnippet: "Ask the user a question when required information is missing.",
|
|
374
544
|
promptGuidelines: [
|
|
375
545
|
"Use question only when you cannot proceed safely or accurately without user input.",
|
|
@@ -379,6 +549,7 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
379
549
|
"Pass a short summary via context when the question depends on prior findings.",
|
|
380
550
|
"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
551
|
"Do not use question for information you can infer, inspect, or compute with other tools.",
|
|
552
|
+
"For independent related decisions, send one question call with a questions array. The user can use Tab/Shift+Tab or Left/Right to revisit pages, then presses Enter on the review page to submit all answers.",
|
|
382
553
|
],
|
|
383
554
|
// Block other tool calls in the same turn until the user answers, so the
|
|
384
555
|
// model can't batch question with side-effecting tools and run them first.
|
|
@@ -386,6 +557,53 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
386
557
|
parameters: QuestionParamsSchema,
|
|
387
558
|
|
|
388
559
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
560
|
+
if (Array.isArray(params.questions)) {
|
|
561
|
+
const prepared = prepareBatchQuestions(params.questions);
|
|
562
|
+
if ("error" in prepared) {
|
|
563
|
+
return { content: [{ type: "text", text: prepared.error }], isError: true, details: { questions: [], answers: [], cancelled: true } satisfies BatchQuestionDetails };
|
|
564
|
+
}
|
|
565
|
+
const questions = prepared.questions;
|
|
566
|
+
const detailsBase = {
|
|
567
|
+
questions: questions.map(({ id, label, question, context, options }) => ({ id, label, question, context, options })),
|
|
568
|
+
};
|
|
569
|
+
if (signal?.aborted) {
|
|
570
|
+
return { content: [{ type: "text", text: "Question batch cancelled" }], details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const protocol = createTUIProtocol(ctx);
|
|
574
|
+
if (!protocol.hasUI) {
|
|
575
|
+
return {
|
|
576
|
+
content: [{ type: "text", text: "Question batch requires interactive mode." }],
|
|
577
|
+
isError: true,
|
|
578
|
+
details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
onUpdate?.({ content: [{ type: "text", text: "Waiting for answers to question batch..." }], details: { ...detailsBase, answers: [], cancelled: false } satisfies BatchQuestionDetails });
|
|
582
|
+
|
|
583
|
+
let answers: BatchAnswer[] | null | undefined;
|
|
584
|
+
try {
|
|
585
|
+
const customFactory: CustomFactory<BatchAnswer[] | null> = (tui, theme, keybindings, done) => {
|
|
586
|
+
if (signal) signal.addEventListener("abort", () => done(null), { once: true });
|
|
587
|
+
const timeout = params.timeout as number | undefined;
|
|
588
|
+
if (timeout && timeout > 0) setTimeout(() => done(null), timeout);
|
|
589
|
+
return new BatchQuestionComponent(questions, tui, theme, keybindings, done);
|
|
590
|
+
};
|
|
591
|
+
answers = await protocol.custom(customFactory);
|
|
592
|
+
if (answers === undefined) answers = await askBatchViaDialogs(questions, protocol, params.timeout as number | undefined);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
const message = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
|
|
595
|
+
return { content: [{ type: "text", text: `Question batch failed: ${message}` }], isError: true, details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (!answers) {
|
|
599
|
+
return { content: [{ type: "text", text: "User cancelled the question batch" }], details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails };
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
content: [{ type: "text", text: answers.map((answer) => `${answer.id}: ${formatResponseSummary(answer.response)}`).join("\n") }],
|
|
603
|
+
details: { ...detailsBase, answers, cancelled: false } satisfies BatchQuestionDetails,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
389
607
|
if (signal?.aborted) {
|
|
390
608
|
return {
|
|
391
609
|
content: [{ type: "text", text: "Question cancelled" }],
|
|
@@ -451,7 +669,7 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
451
669
|
let result: QuestionResponse | null;
|
|
452
670
|
|
|
453
671
|
try {
|
|
454
|
-
const customFactory: CustomFactory = (
|
|
672
|
+
const customFactory: CustomFactory<QuestionResponse | null> = (
|
|
455
673
|
tui: TUI,
|
|
456
674
|
theme: Theme,
|
|
457
675
|
keybindings: KeybindingsManager,
|
|
@@ -514,6 +732,15 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
514
732
|
},
|
|
515
733
|
|
|
516
734
|
renderCall(args, theme, _context) {
|
|
735
|
+
const batch = Array.isArray(args.questions) ? args.questions : undefined;
|
|
736
|
+
if (batch) {
|
|
737
|
+
const labels = batch.map((item: { label?: string; id?: string }, index: number) => item.label || item.id || `Q${index + 1}`).join(", ");
|
|
738
|
+
return new Text(
|
|
739
|
+
`${theme.fg("toolTitle", theme.bold("QUESTION"))} ${theme.fg("accent", theme.bold(`${batch.length} question${batch.length === 1 ? "" : "s"}`))}${labels ? theme.fg("dim", ` · ${oneLine(labels, 100)}`) : ""}`,
|
|
740
|
+
0,
|
|
741
|
+
0,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
517
744
|
const question = (args.question as string) || "";
|
|
518
745
|
const rawOptions = Array.isArray(args.options) ? args.options : [];
|
|
519
746
|
const title = args.context ? `${args.context}: ` : args.title ? `${args.title}: ` : "";
|
|
@@ -532,7 +759,7 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
532
759
|
},
|
|
533
760
|
|
|
534
761
|
renderResult(result, options, theme, _context) {
|
|
535
|
-
const details = result.details as (QuestionDetails & { error?: string }
|
|
762
|
+
const details = result.details as (QuestionDetails | BatchQuestionDetails) & { error?: string } | undefined;
|
|
536
763
|
|
|
537
764
|
if (details?.error) return new Text(theme.fg("error", details.error), 0, 0);
|
|
538
765
|
|
|
@@ -546,9 +773,12 @@ export default function questionExtension(pi: ExtensionAPI) {
|
|
|
546
773
|
return new Text(theme.fg("muted", waitingText), 0, 0);
|
|
547
774
|
}
|
|
548
775
|
|
|
549
|
-
if (!details || details.cancelled
|
|
550
|
-
|
|
776
|
+
if (!details || details.cancelled) return new Text(theme.fg("warning", "question cancelled"), 0, 0);
|
|
777
|
+
|
|
778
|
+
if ("answers" in details) {
|
|
779
|
+
return new Text(details.answers.map((answer) => `${theme.fg("success", "✓")} ${theme.fg("accent", answer.id)}: ${theme.fg("text", oneLine(formatResponseSummary(answer.response), 120))}`).join("\n"), 0, 0);
|
|
551
780
|
}
|
|
781
|
+
if (!details.response) return new Text(theme.fg("warning", "question cancelled"), 0, 0);
|
|
552
782
|
|
|
553
783
|
const response = details.response;
|
|
554
784
|
let text = theme.fg("success", theme.bold("answered: "));
|
|
@@ -10,35 +10,48 @@ const OptionSchema = Type.Union([
|
|
|
10
10
|
}),
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const QuestionFields = {
|
|
14
14
|
question: Type.String({ description: "Question to ask the user before continuing" }),
|
|
15
|
-
title: Type.Optional(Type.String({ description: "Short title for the question UI (deprecated alias
|
|
16
|
-
context: Type.Optional(
|
|
17
|
-
Type.String({ description: "Relevant context summary shown before the question" }),
|
|
18
|
-
),
|
|
15
|
+
title: Type.Optional(Type.String({ description: "Short title for the question UI (deprecated alias for context summary)" })),
|
|
16
|
+
context: Type.Optional(Type.String({ description: "Relevant context summary shown before the question" })),
|
|
19
17
|
options: Type.Optional(
|
|
20
18
|
Type.Array(OptionSchema, {
|
|
21
19
|
description: "Optional multiple-choice options. Each option may be a string or { label, description }.",
|
|
22
20
|
}),
|
|
23
21
|
),
|
|
24
|
-
allowMultiple: Type.Optional(
|
|
25
|
-
Type.Boolean({ description: "Allow selecting multiple options. Default: false" }),
|
|
26
|
-
),
|
|
22
|
+
allowMultiple: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options. Default: false" })),
|
|
27
23
|
allowFreeform: Type.Optional(
|
|
28
24
|
Type.Boolean({
|
|
29
25
|
description: "Allow a custom freeform answer. Defaults to true when no options are provided, false when options exist.",
|
|
30
26
|
}),
|
|
31
27
|
),
|
|
32
|
-
allowComment: Type.Optional(
|
|
33
|
-
Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" }),
|
|
34
|
-
),
|
|
28
|
+
allowComment: Type.Optional(Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" })),
|
|
35
29
|
commentToggleKey: Type.Optional(
|
|
36
30
|
Type.String({
|
|
37
31
|
description:
|
|
38
32
|
"Shortcut for toggling the optional comment/extra-context row when allowComment is true, e.g. 'ctrl+g'. Pass 'off' to disable. Default: PI_QUESTION_COMMENT_TOGGLE_KEY env var if set, otherwise 'ctrl+g'.",
|
|
39
33
|
}),
|
|
40
34
|
),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SingleQuestionParamsSchema = Type.Object({
|
|
38
|
+
...QuestionFields,
|
|
39
|
+
timeout: Type.Optional(Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." })),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const BatchQuestionSchema = Type.Object({
|
|
43
|
+
id: Type.Optional(Type.String({ description: "Unique answer id. Defaults to q1, q2, and so on." })),
|
|
44
|
+
label: Type.Optional(Type.String({ description: "Short page label. Defaults to Q1, Q2, and so on." })),
|
|
45
|
+
...QuestionFields,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const BatchQuestionParamsSchema = Type.Object({
|
|
49
|
+
questions: Type.Array(BatchQuestionSchema, {
|
|
50
|
+
minItems: 1,
|
|
51
|
+
description:
|
|
52
|
+
"Questions to answer in one paged form. User can move between pages with Tab/Shift+Tab or Left/Right, then press Enter on the final review page to submit all answers.",
|
|
53
|
+
}),
|
|
54
|
+
timeout: Type.Optional(Type.Number({ description: "Auto-dismiss the whole batch after N milliseconds." })),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const QuestionParamsSchema = Type.Union([SingleQuestionParamsSchema, BatchQuestionParamsSchema]);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { prepareBatchQuestions } from "../batch.ts";
|
|
5
|
+
|
|
6
|
+
test("prepareBatchQuestions: normalizes defaults and generated ids", () => {
|
|
7
|
+
const result = prepareBatchQuestions([
|
|
8
|
+
{ question: "Pick a color", options: ["Red", "Blue"] },
|
|
9
|
+
{ id: "notes", label: "Notes", question: "Anything else?" },
|
|
10
|
+
]);
|
|
11
|
+
assert.ok("questions" in result);
|
|
12
|
+
if (!("questions" in result)) return;
|
|
13
|
+
assert.deepEqual(
|
|
14
|
+
result.questions.map(({ id, label, question, options, allowFreeform }) => ({ id, label, question, options, allowFreeform })),
|
|
15
|
+
[
|
|
16
|
+
{ id: "q1", label: "Q1", question: "Pick a color", options: [{ label: "Red" }, { label: "Blue" }], allowFreeform: false },
|
|
17
|
+
{ id: "notes", label: "Notes", question: "Anything else?", options: [], allowFreeform: true },
|
|
18
|
+
],
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("prepareBatchQuestions: rejects empty batches, duplicate ids, and impossible questions", () => {
|
|
23
|
+
assert.deepEqual(prepareBatchQuestions([]), { error: "questions must contain at least one question." });
|
|
24
|
+
assert.match((prepareBatchQuestions([{ id: "same", question: "One" }, { id: "same", question: "Two" }]) as { error: string }).error, /duplicate id/);
|
|
25
|
+
assert.match((prepareBatchQuestions([{ question: "Choose", allowFreeform: false }]) as { error: string }).error, /options are empty/);
|
|
26
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
|
-
import { DialogFallback } from "../dialog-adapter.ts";
|
|
5
|
-
import type { QuestionResponse, ResolvedQuestionParams } from "../types.ts";
|
|
4
|
+
import { askBatchViaDialogs, DialogFallback } from "../dialog-adapter.ts";
|
|
5
|
+
import type { BatchedQuestion, QuestionResponse, ResolvedQuestionParams } from "../types.ts";
|
|
6
6
|
import type { UIProtocol } from "../ui-protocol.ts";
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -134,4 +134,24 @@ test("DialogFallback: freeform cancel (input returns undefined after selecting c
|
|
|
134
134
|
});
|
|
135
135
|
const result = await DialogFallback.ask(makeParams({ allowFreeform: true }), protocol);
|
|
136
136
|
assert.equal(result, null);
|
|
137
|
-
});
|
|
137
|
+
});
|
|
138
|
+
test("askBatchViaDialogs: returns identified answers and passes remaining timeout", async () => {
|
|
139
|
+
const timeouts: Array<number | undefined> = [];
|
|
140
|
+
const protocol = fakeProtocol({
|
|
141
|
+
select: async (_prompt, _options, opts) => {
|
|
142
|
+
timeouts.push(opts?.timeout);
|
|
143
|
+
return "Option A";
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const questions: BatchedQuestion[] = [
|
|
147
|
+
{ ...makeParams(), id: "first", label: "First" },
|
|
148
|
+
{ ...makeParams({ question: "Pick again" }), id: "second", label: "Second" },
|
|
149
|
+
];
|
|
150
|
+
const result = await askBatchViaDialogs(questions, protocol, 1_000);
|
|
151
|
+
assert.deepEqual(result, [
|
|
152
|
+
{ id: "first", question: "Pick one", response: { kind: "selection", selections: ["Option A"] } },
|
|
153
|
+
{ id: "second", question: "Pick again", response: { kind: "selection", selections: ["Option A"] } },
|
|
154
|
+
]);
|
|
155
|
+
assert.equal(timeouts.length, 2);
|
|
156
|
+
assert.ok(timeouts.every((timeout) => timeout !== undefined && timeout > 0 && timeout <= 1_000));
|
|
157
|
+
});
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ExtensionContext } from "@selesai/code";
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
import type { UIProtocol } from "./ui-protocol.ts";
|
|
5
|
+
import type { CustomFactory, UIProtocol } from "./ui-protocol.ts";
|
|
7
6
|
|
|
8
7
|
export function createTUIProtocol(ctx: ExtensionContext): UIProtocol {
|
|
9
8
|
return {
|
|
10
9
|
hasUI: ctx.hasUI,
|
|
11
10
|
theme: ctx.ui.theme,
|
|
12
|
-
custom: (factory) => ctx.ui.custom(factory as any) as Promise<
|
|
11
|
+
custom: <T>(factory: CustomFactory<T>) => ctx.ui.custom(factory as any) as Promise<T | undefined>,
|
|
13
12
|
select: (prompt, options, opts) => ctx.ui.select(prompt, options, opts),
|
|
14
13
|
input: (prompt, placeholder, opts) => ctx.ui.input(prompt, placeholder, opts),
|
|
15
14
|
};
|
|
@@ -32,6 +32,43 @@ export interface QuestionParams {
|
|
|
32
32
|
timeout?: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface RawBatchQuestion {
|
|
36
|
+
id?: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
question: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
context?: string;
|
|
41
|
+
options?: RawOption[];
|
|
42
|
+
allowMultiple?: boolean;
|
|
43
|
+
allowFreeform?: boolean;
|
|
44
|
+
allowComment?: boolean;
|
|
45
|
+
commentToggleKey?: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BatchedQuestion {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
question: string;
|
|
52
|
+
context: string | undefined;
|
|
53
|
+
options: QuestionOption[];
|
|
54
|
+
allowMultiple: boolean;
|
|
55
|
+
allowFreeform: boolean;
|
|
56
|
+
allowComment: boolean;
|
|
57
|
+
shortcuts: ResolvedShortcuts;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BatchAnswer {
|
|
61
|
+
id: string;
|
|
62
|
+
question: string;
|
|
63
|
+
response: QuestionResponse;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BatchQuestionDetails {
|
|
67
|
+
questions: Array<Pick<BatchedQuestion, "id" | "label" | "question" | "context" | "options">>;
|
|
68
|
+
answers: BatchAnswer[];
|
|
69
|
+
cancelled: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
35
72
|
// ---------------------------------------------------------------------------
|
|
36
73
|
// Shortcut resolution
|
|
37
74
|
// ---------------------------------------------------------------------------
|
|
@@ -11,17 +11,17 @@ export interface CustomFactoryResult {
|
|
|
11
11
|
focused: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export type CustomFactory = (
|
|
14
|
+
export type CustomFactory<T> = (
|
|
15
15
|
tui: TUI,
|
|
16
16
|
theme: Theme,
|
|
17
17
|
keybindings: KeybindingsManager,
|
|
18
|
-
done: (result:
|
|
18
|
+
done: (result: T) => void,
|
|
19
19
|
) => CustomFactoryResult;
|
|
20
20
|
|
|
21
21
|
export interface UIProtocol {
|
|
22
22
|
readonly hasUI: boolean;
|
|
23
23
|
readonly theme: Theme;
|
|
24
|
-
custom(factory: CustomFactory): Promise<
|
|
24
|
+
custom<T>(factory: CustomFactory<T>): Promise<T | undefined>;
|
|
25
25
|
select(prompt: string, options: string[], opts?: { timeout?: number }): Promise<string | undefined>;
|
|
26
26
|
input(prompt: string, placeholder?: string, opts?: { timeout?: number }): Promise<string | undefined>;
|
|
27
27
|
}
|
|
@@ -6,7 +6,9 @@ disable-model-invocation: true
|
|
|
6
6
|
|
|
7
7
|
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
|
8
8
|
|
|
9
|
-
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask *now* without guessing at answers you haven't heard yet.
|
|
9
|
+
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask *now* without guessing at answers you haven't heard yet.
|
|
10
|
+
|
|
11
|
+
For every non-empty frontier, call the `question` tool **exactly once** with its `questions` array; never print frontier questions as plain chat text. Give every item a stable `id`, short `label`, concise `question`, and `context` containing the recommended answer and why. Provide options when known and set `allowFreeform: true` when the user may need another answer. The batch UI lets the user revisit pages before pressing Enter on its final review page to submit all answers. Wait for that tool result, map returned `answers` by `id`, then recompute the next frontier.
|
|
10
12
|
|
|
11
13
|
Each round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a *later* round, not this one.
|
|
12
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.21",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"playwright": "^1.60.0",
|
|
75
75
|
"proper-lockfile": "4.1.2",
|
|
76
76
|
"semver": "7.8.0",
|
|
77
|
+
"tsx": "^4.20.0",
|
|
77
78
|
"typebox": "1.1.38",
|
|
78
79
|
"undici": "8.5.0",
|
|
79
80
|
"yaml": "2.9.0"
|