@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.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/CHANGELOG.md +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -1,475 +1,241 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @pi-unipi/ask-user —
|
|
2
|
+
* @pi-unipi/ask-user — the ask_user tool
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* One call asks 1–4 questions (Devin's shape). Runs sequentially: pi runs a
|
|
5
|
+
* message's tool calls in parallel by default, and several dialogs at once
|
|
6
|
+
* used to hide all but the last — the rest could never be answered and the
|
|
7
|
+
* turn hung. One dialog at a time, every question in one call.
|
|
5
8
|
*/
|
|
6
9
|
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
7
11
|
import { Type } from "typebox";
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
emitEvent,
|
|
14
|
-
withHerdrBlocked,
|
|
15
|
-
} from "@pi-unipi/core";
|
|
16
|
-
import type { NormalizedOption, AskUserResponse, SessionLauncherResult } from "./types.js";
|
|
17
|
-
import { renderAskUI, createRenderCall, createRenderResult } from "./ask-ui.js";
|
|
18
|
-
import { renderLauncherUI } from "./launcher-ui.js";
|
|
19
|
-
import { getAskUserSettings } from "./config.js";
|
|
12
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { ASK_USER_TOOLS, COMPACTOR_INSTRUCTION, emitEvent, UNIPI_EVENTS, withHerdrBlocked, type Attachment } from "@pi-unipi/core";
|
|
15
|
+
import { AskPanel, type PanelResult } from "./ask-ui.js";
|
|
16
|
+
import { getAskUserSettings, type AskUserSettings } from "./config.js";
|
|
20
17
|
import { queueCompactHandoff, queueDirectHandoff } from "./handoff.js";
|
|
18
|
+
import { renderLauncherUI } from "./launcher-ui.js";
|
|
19
|
+
import { answerSummary, answersText, clarifyText, HEADER_MAX, prepareArgs, type AskParams, type AskQuestion, type QuestionAnswer } from "./questions.js";
|
|
20
|
+
import type { SessionLauncherResult } from "./types.js";
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Whether this process is a subagent child (subagents package or fusion sidekick).
|
|
24
|
-
* Subagent children never talk to the user directly — their lead owns ambiguity.
|
|
25
|
-
*/
|
|
22
|
+
/** Subagent children never talk to the user directly — their lead owns ambiguity. */
|
|
26
23
|
export function isSubagentChild(env: Record<string, string | undefined> = process.env): boolean {
|
|
27
24
|
return env.UNIPI_SUBAGENT_CHILD === "1";
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
export interface AskDetails {
|
|
28
|
+
questions: AskQuestion[];
|
|
29
|
+
answers?: QuestionAnswer[];
|
|
30
|
+
outcome: "answered" | "clarify" | "cancelled" | "action" | "unavailable";
|
|
31
|
+
/** Legacy single-question fields, kept for older consumers (compactor, notify). */
|
|
32
|
+
question?: string;
|
|
33
|
+
attachments?: Array<Pick<Attachment, "id" | "kind" | "path" | "name">>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const OPTION = Type.Object({
|
|
37
|
+
label: Type.String({ description: "Display text (1–5 words)" }),
|
|
38
|
+
description: Type.Optional(Type.String({ description: "What this option means or its trade-offs" })),
|
|
39
|
+
value: Type.Optional(Type.String({ description: "Returned instead of the label when set" })),
|
|
40
|
+
action: Type.Optional(Type.Union([Type.Literal("end_turn"), Type.Literal("new_session")], {
|
|
41
|
+
description: "end_turn: stop the turn when picked · new_session: hand off to `prefill` (compact & run / run directly)",
|
|
42
|
+
})),
|
|
43
|
+
prefill: Type.Optional(Type.String({ description: "Message or /command queued by a new_session option" })),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const QUESTION = Type.Object({
|
|
47
|
+
question: Type.String({ description: "The full question" }),
|
|
48
|
+
header: Type.String({ description: `Short chip label, e.g. "Database" (≤${HEADER_MAX} chars)` }),
|
|
49
|
+
options: Type.Array(OPTION, { description: "2–4 choices. An \"Other\" free-text choice is added automatically." }),
|
|
50
|
+
multi_select: Type.Optional(Type.Boolean({ description: "Let the user pick several (default false)" })),
|
|
51
|
+
other: Type.Optional(Type.Boolean({ description: "Offer \"Other (type your own)\" (default true)" })),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function unavailable(questions: AskQuestion[], text: string) {
|
|
55
|
+
return { content: [{ type: "text" as const, text }], details: { questions, outcome: "unavailable" } as AskDetails };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function imageParts(attachments: readonly Attachment[]) {
|
|
59
|
+
const parts: Array<{ type: "image"; data: string; mimeType: string }> = [];
|
|
60
|
+
for (const a of attachments) {
|
|
61
|
+
if (a.kind !== "image") continue;
|
|
62
|
+
try {
|
|
63
|
+
parts.push({ type: "image", data: readFileSync(a.path).toString("base64"), mimeType: a.mimeType ?? "image/png" });
|
|
64
|
+
} catch {
|
|
65
|
+
// unreadable: the token stays in the text
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** "[File #2]" → "[File #2: /path]" so the agent can open documents. */
|
|
72
|
+
function expandFileTokens(answers: QuestionAnswer[], attachments: readonly Attachment[]): QuestionAnswer[] {
|
|
73
|
+
const files = attachments.filter((a) => a.kind === "file");
|
|
74
|
+
if (!files.length) return answers;
|
|
75
|
+
return answers.map((a) => (a.custom_text ? { ...a, custom_text: files.reduce((t, f) => t.split(`[File #${f.id}]`).join(`[File #${f.id}: ${f.path}]`), a.custom_text) } : a));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The user's settings win over what the agent asked for. */
|
|
79
|
+
export function applySettings(questions: AskQuestion[], settings: AskUserSettings): AskQuestion[] {
|
|
80
|
+
return questions.slice(0, settings.maxQuestions).map((q) => {
|
|
81
|
+
if (settings.other === "always" || q.options.length === 0) {
|
|
82
|
+
const { other: _drop, ...rest } = q;
|
|
83
|
+
return rest;
|
|
84
|
+
}
|
|
85
|
+
return settings.other === "never" ? { ...q, other: false } : q;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
33
89
|
export function registerAskUserTools(pi: ExtensionAPI): void {
|
|
90
|
+
const max = getAskUserSettings().maxQuestions;
|
|
34
91
|
pi.registerTool({
|
|
35
92
|
name: ASK_USER_TOOLS.ASK,
|
|
36
93
|
label: "Ask User",
|
|
37
94
|
description:
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
promptSnippet: "Ask the user
|
|
95
|
+
`Ask the user 1–${max} multiple-choice question${max === 1 ? "" : "s"} in one dialog and wait for the answers. ` +
|
|
96
|
+
"Each question has a short header, 2–4 options (label + description) and optional multi-select; " +
|
|
97
|
+
"an \"Other\" free-text choice is always added. The user may skip questions.",
|
|
98
|
+
promptSnippet: "Ask the user multiple-choice questions and wait for the answers.",
|
|
42
99
|
promptGuidelines: [
|
|
43
|
-
"Use ask_user when
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"Use
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"Use action: 'new_session' with prefill to let the user hand off to a queued follow-up message or slash command."
|
|
100
|
+
"Use ask_user when a decision, preference or clarification needs the user before you continue.",
|
|
101
|
+
`Put every question you need right now in ONE call (1–${max} question${max === 1 ? "" : "s"}) — never several ask_user calls at once.`,
|
|
102
|
+
"Give each question a short header (≤16 chars) and 2–4 options with a clear description; don't add an 'Other' option yourself.",
|
|
103
|
+
"Use multi_select when several answers can apply.",
|
|
104
|
+
"Skipped questions come back as skipped: respect that, don't ask them again unless you must.",
|
|
105
|
+
"If the user says they're not ready, ask what they want to clarify instead of re-asking.",
|
|
106
|
+
"Options can carry action: 'end_turn' or action: 'new_session' with a prefill for workflow handoffs.",
|
|
51
107
|
],
|
|
52
108
|
parameters: Type.Object({
|
|
53
|
-
|
|
54
|
-
description: "The question to ask the user",
|
|
55
|
-
}),
|
|
56
|
-
context: Type.Optional(
|
|
57
|
-
Type.String({
|
|
58
|
-
description: "Additional context shown before the question",
|
|
59
|
-
}),
|
|
60
|
-
),
|
|
61
|
-
options: Type.Optional(
|
|
62
|
-
Type.Array(
|
|
63
|
-
Type.Object({
|
|
64
|
-
label: Type.String({ description: "Display label" }),
|
|
65
|
-
description: Type.Optional(
|
|
66
|
-
Type.String({ description: "Optional description shown below label" }),
|
|
67
|
-
),
|
|
68
|
-
value: Type.Optional(
|
|
69
|
-
Type.String({
|
|
70
|
-
description: "Value returned when selected (defaults to label)",
|
|
71
|
-
}),
|
|
72
|
-
),
|
|
73
|
-
allowCustom: Type.Optional(
|
|
74
|
-
Type.Boolean({
|
|
75
|
-
description:
|
|
76
|
-
"When true, selecting this option enters text input mode " +
|
|
77
|
-
"so the user can add a custom comment before submitting.",
|
|
78
|
-
}),
|
|
79
|
-
),
|
|
80
|
-
action: Type.Optional(
|
|
81
|
-
Type.Union(
|
|
82
|
-
[
|
|
83
|
-
Type.Literal("select"),
|
|
84
|
-
Type.Literal("input"),
|
|
85
|
-
Type.Literal("end_turn"),
|
|
86
|
-
Type.Literal("new_session"),
|
|
87
|
-
],
|
|
88
|
-
{
|
|
89
|
-
description:
|
|
90
|
-
"Special action: 'select' (default), 'input' (text input), " +
|
|
91
|
-
"'end_turn' (signal end of turn), 'new_session' (queue handoff with prefill).",
|
|
92
|
-
},
|
|
93
|
-
),
|
|
94
|
-
),
|
|
95
|
-
prefill: Type.Optional(
|
|
96
|
-
Type.String({
|
|
97
|
-
description: "Prefill message for new_session action.",
|
|
98
|
-
}),
|
|
99
|
-
),
|
|
100
|
-
}),
|
|
101
|
-
{
|
|
102
|
-
description:
|
|
103
|
-
"Multiple-choice options. Omit for freeform-only input.",
|
|
104
|
-
},
|
|
105
|
-
),
|
|
106
|
-
),
|
|
107
|
-
allowMultiple: Type.Optional(
|
|
108
|
-
Type.Boolean({
|
|
109
|
-
description: "Enable multi-select mode (default: false)",
|
|
110
|
-
}),
|
|
111
|
-
),
|
|
112
|
-
allowFreeform: Type.Optional(
|
|
113
|
-
Type.Boolean({
|
|
114
|
-
description: "Allow freeform text input (default: true)",
|
|
115
|
-
}),
|
|
116
|
-
),
|
|
117
|
-
timeout: Type.Optional(
|
|
118
|
-
Type.Number({
|
|
119
|
-
description: "Auto-dismiss after N milliseconds",
|
|
120
|
-
}),
|
|
121
|
-
),
|
|
109
|
+
questions: Type.Array(QUESTION, { description: `1–${max} question${max === 1 ? "" : "s"}` }),
|
|
122
110
|
}),
|
|
111
|
+
// Older calls ({ question, context, options, allowMultiple, allowFreeform, timeout }) still work.
|
|
112
|
+
prepareArguments: (args: unknown) => prepareArgs(args) as never,
|
|
113
|
+
executionMode: "sequential",
|
|
123
114
|
|
|
124
115
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx: ExtensionContext) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
context,
|
|
128
|
-
options: rawOptions,
|
|
129
|
-
allowMultiple = false,
|
|
130
|
-
allowFreeform = true,
|
|
131
|
-
timeout,
|
|
132
|
-
} = params as {
|
|
133
|
-
question: string;
|
|
134
|
-
context?: string;
|
|
135
|
-
options?: { label: string; description?: string; value?: string; allowCustom?: boolean; action?: string; prefill?: string }[];
|
|
136
|
-
allowMultiple?: boolean;
|
|
137
|
-
allowFreeform?: boolean;
|
|
138
|
-
timeout?: number;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Subagent children have no user to talk to. Refuse loudly so the
|
|
142
|
-
// subagent surfaces the question to its lead instead of guessing.
|
|
116
|
+
const settings = getAskUserSettings(ctx.cwd);
|
|
117
|
+
const questions = applySettings((prepareArgs(params) as AskParams).questions, settings);
|
|
143
118
|
if (isSubagentChild()) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
type: "text",
|
|
148
|
-
text:
|
|
149
|
-
"ask_user is not available inside a subagent. You cannot talk to the user directly — only your lead can. Do not guess or assume an answer. Stop and state the question, the options you considered, and your recommendation in your report so your lead can decide.",
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
isError: true,
|
|
153
|
-
details: {
|
|
154
|
-
question,
|
|
155
|
-
response: {
|
|
156
|
-
kind: "cancelled",
|
|
157
|
-
comment: "ask_user is not available inside a subagent",
|
|
158
|
-
} as AskUserResponse,
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Check settings
|
|
164
|
-
const settings = getAskUserSettings();
|
|
165
|
-
if (!settings.enabled) {
|
|
166
|
-
return {
|
|
167
|
-
content: [
|
|
168
|
-
{
|
|
169
|
-
type: "text",
|
|
170
|
-
text: "Error: ask_user tool is disabled in settings.",
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
details: {
|
|
174
|
-
question,
|
|
175
|
-
response: {
|
|
176
|
-
kind: "cancelled",
|
|
177
|
-
comment: "Tool disabled",
|
|
178
|
-
} as AskUserResponse,
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Validate requested format against allowed formats
|
|
184
|
-
if (allowMultiple && !settings.allowedFormats.multiSelect) {
|
|
185
|
-
return {
|
|
186
|
-
content: [
|
|
187
|
-
{
|
|
188
|
-
type: "text",
|
|
189
|
-
text: "Error: Multi-select questions are disabled in settings.",
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
details: {
|
|
193
|
-
question,
|
|
194
|
-
response: {
|
|
195
|
-
kind: "cancelled",
|
|
196
|
-
comment: "Multi-select disabled",
|
|
197
|
-
} as AskUserResponse,
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!allowMultiple && !settings.allowedFormats.singleSelect) {
|
|
203
|
-
return {
|
|
204
|
-
content: [
|
|
205
|
-
{
|
|
206
|
-
type: "text",
|
|
207
|
-
text: "Error: Single-select questions are disabled in settings.",
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
details: {
|
|
211
|
-
question,
|
|
212
|
-
response: {
|
|
213
|
-
kind: "cancelled",
|
|
214
|
-
comment: "Single-select disabled",
|
|
215
|
-
} as AskUserResponse,
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (allowFreeform && !settings.allowedFormats.freeform) {
|
|
221
|
-
return {
|
|
222
|
-
content: [
|
|
223
|
-
{
|
|
224
|
-
type: "text",
|
|
225
|
-
text: "Error: Freeform questions are disabled in settings.",
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
details: {
|
|
229
|
-
question,
|
|
230
|
-
response: {
|
|
231
|
-
kind: "cancelled",
|
|
232
|
-
comment: "Freeform disabled",
|
|
233
|
-
} as AskUserResponse,
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Validate: need UI
|
|
239
|
-
if (!ctx.hasUI) {
|
|
240
|
-
return {
|
|
241
|
-
content: [
|
|
242
|
-
{
|
|
243
|
-
type: "text",
|
|
244
|
-
text: "Error: UI not available (running in non-interactive mode)",
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
details: {
|
|
248
|
-
question,
|
|
249
|
-
response: {
|
|
250
|
-
kind: "cancelled",
|
|
251
|
-
comment: "No UI available",
|
|
252
|
-
} as AskUserResponse,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Validate: need options or freeform
|
|
258
|
-
const options = rawOptions || [];
|
|
259
|
-
if (options.length === 0 && !allowFreeform) {
|
|
260
|
-
return {
|
|
261
|
-
content: [
|
|
262
|
-
{
|
|
263
|
-
type: "text",
|
|
264
|
-
text: "Error: No options provided and allowFreeform is false. Provide options or enable freeform.",
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
details: {
|
|
268
|
-
question,
|
|
269
|
-
response: {
|
|
270
|
-
kind: "cancelled",
|
|
271
|
-
comment: "No options and no freeform",
|
|
272
|
-
} as AskUserResponse,
|
|
273
|
-
},
|
|
274
|
-
};
|
|
119
|
+
throw new Error(
|
|
120
|
+
"ask_user is not available inside a subagent. You cannot talk to the user directly — only your lead can. Do not guess an answer: state the question, the options you considered and your recommendation in your report.",
|
|
121
|
+
);
|
|
275
122
|
}
|
|
123
|
+
if (!settings.enabled) return unavailable(questions, "ask_user is turned off in settings — ask in your reply instead.");
|
|
124
|
+
if (!ctx.hasUI) return unavailable(questions, "No interactive UI (non-interactive mode) — ask in your reply instead.");
|
|
125
|
+
if (questions.length === 0) throw new Error("ask_user needs at least one question with a question text.");
|
|
276
126
|
|
|
277
|
-
// Normalize options — resolve value (defaults to label)
|
|
278
|
-
const normalizedOptions: NormalizedOption[] = options.map((opt) => ({
|
|
279
|
-
label: opt.label,
|
|
280
|
-
description: opt.description,
|
|
281
|
-
value: opt.value ?? opt.label,
|
|
282
|
-
allowCustom: opt.allowCustom ?? false,
|
|
283
|
-
action: (opt.action as NormalizedOption["action"]) ?? "select",
|
|
284
|
-
prefill: opt.prefill,
|
|
285
|
-
}));
|
|
286
|
-
const detailsBase = {
|
|
287
|
-
question,
|
|
288
|
-
context,
|
|
289
|
-
options: normalizedOptions,
|
|
290
|
-
allowMultiple,
|
|
291
|
-
allowFreeform,
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// Emit ASK_USER_PROMPT event if notifyOnAsk is enabled
|
|
295
127
|
if (settings.notifyOnAsk) {
|
|
296
128
|
emitEvent(pi, UNIPI_EVENTS.ASK_USER_PROMPT, {
|
|
297
|
-
question,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
allowFreeform,
|
|
129
|
+
question: questions.map((q) => q.question).join(" · "),
|
|
130
|
+
optionCount: questions.reduce((n, q) => n + q.options.length, 0),
|
|
131
|
+
allowMultiple: questions.some((q) => q.multi_select),
|
|
132
|
+
allowFreeform: questions.some((q) => q.other !== false),
|
|
302
133
|
});
|
|
303
134
|
}
|
|
304
135
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"ask_user",
|
|
309
|
-
() => ctx.ui.custom<{ response: AskUserResponse } | null>(
|
|
310
|
-
renderAskUI({
|
|
311
|
-
question,
|
|
312
|
-
context,
|
|
313
|
-
options: normalizedOptions,
|
|
314
|
-
allowMultiple,
|
|
315
|
-
allowFreeform,
|
|
316
|
-
timeout,
|
|
317
|
-
}),
|
|
318
|
-
),
|
|
136
|
+
const result = await withHerdrBlocked(pi, "ask_user", () =>
|
|
137
|
+
ctx.ui.custom<PanelResult>((tui, theme, _kb, done) =>
|
|
138
|
+
new AskPanel(tui, theme, questions, done, undefined, { escape: settings.escape, digitAdvance: settings.digitAdvance, helpLine: settings.helpLine })),
|
|
319
139
|
);
|
|
140
|
+
const legacy = questions.length === 1 ? { question: questions[0]!.question } : {};
|
|
320
141
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
{
|
|
326
|
-
type: "text",
|
|
327
|
-
text: "User cancelled the selection",
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
details: {
|
|
331
|
-
...detailsBase,
|
|
332
|
-
response: {
|
|
333
|
-
kind: "cancelled",
|
|
334
|
-
} as AskUserResponse,
|
|
335
|
-
},
|
|
336
|
-
};
|
|
142
|
+
if (!result || result.type === "cancel") {
|
|
143
|
+
// Esc stops the turn, like Devin's "Canceled due to user interrupt" —
|
|
144
|
+
// `terminate` ends it after this call, cleanly (no abort error line).
|
|
145
|
+
return { content: [{ type: "text" as const, text: "The user cancelled the questions and stopped the turn. Wait for their next message." }], details: { questions, outcome: "cancelled", ...legacy } as AskDetails, terminate: true };
|
|
337
146
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const response = result.response;
|
|
341
|
-
let contentText: string;
|
|
342
|
-
|
|
343
|
-
switch (response.kind) {
|
|
344
|
-
case "selection": {
|
|
345
|
-
const selections = response.selections || [];
|
|
346
|
-
contentText =
|
|
347
|
-
selections.length === 1
|
|
348
|
-
? `User selected: ${selections[0]}`
|
|
349
|
-
: `User selected: ${selections.join(", ")}`;
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
case "freeform":
|
|
353
|
-
contentText = `User wrote: ${response.text}`;
|
|
354
|
-
break;
|
|
355
|
-
case "combined": {
|
|
356
|
-
const selections = response.selections || [];
|
|
357
|
-
const selText = selections.length === 1
|
|
358
|
-
? selections[0]
|
|
359
|
-
: selections.join(", ");
|
|
360
|
-
contentText = `User selected: ${selText} and wrote: ${response.text}`;
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
case "end_turn":
|
|
364
|
-
// Abort the agent immediately — no LLM follow-up, no wasted tokens.
|
|
365
|
-
// The tool result is still recorded in session history.
|
|
366
|
-
ctx.abort();
|
|
367
|
-
contentText = "User chose to end the turn.";
|
|
368
|
-
break;
|
|
369
|
-
case "new_session":
|
|
370
|
-
contentText = response.prefill
|
|
371
|
-
? `User chose to hand off to: ${response.prefill}`
|
|
372
|
-
: "User chose a handoff without a prefill.";
|
|
373
|
-
break;
|
|
374
|
-
case "timed_out":
|
|
375
|
-
contentText = "User did not respond (timed out)";
|
|
376
|
-
break;
|
|
377
|
-
default:
|
|
378
|
-
contentText = "No response";
|
|
147
|
+
if (result.type === "clarify") {
|
|
148
|
+
return { content: [{ type: "text" as const, text: clarifyText(questions, result.answers) }], details: { questions, answers: result.answers, outcome: "clarify", ...legacy } as AskDetails };
|
|
379
149
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (response.kind === "new_session") {
|
|
383
|
-
const prefill = response.prefill || "";
|
|
384
|
-
const launcherResult = await withHerdrBlocked(
|
|
385
|
-
pi,
|
|
386
|
-
"ask_user: launch",
|
|
387
|
-
() => ctx.ui.custom<SessionLauncherResult | null>(
|
|
388
|
-
renderLauncherUI({ prefill }),
|
|
389
|
-
),
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
if (!launcherResult || launcherResult.action === "cancel") {
|
|
393
|
-
return {
|
|
394
|
-
content: [{ type: "text", text: "User cancelled the session launch" }],
|
|
395
|
-
details: {
|
|
396
|
-
...detailsBase,
|
|
397
|
-
response: {
|
|
398
|
-
kind: "cancelled",
|
|
399
|
-
comment: "Session launcher cancelled",
|
|
400
|
-
} as AskUserResponse,
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const handoff = launcherResult.action === "compact"
|
|
406
|
-
? queueCompactHandoff({
|
|
407
|
-
pi,
|
|
408
|
-
ctx,
|
|
409
|
-
prefill,
|
|
410
|
-
// Use the compactor sentinel so @pi-unipi/compactor's zero-LLM
|
|
411
|
-
// pipeline intercepts. If compactor is not installed, Pi's built-in
|
|
412
|
-
// LLM-based compaction runs instead.
|
|
413
|
-
customInstructions: `${COMPACTOR_INSTRUCTION}\nPreparing for new task. Summarize previous work concisely, preserving only what's essential for: ${prefill}`,
|
|
414
|
-
})
|
|
415
|
-
: queueDirectHandoff(pi, ctx, prefill);
|
|
416
|
-
|
|
417
|
-
if (handoff.status === "cancelled") {
|
|
418
|
-
return {
|
|
419
|
-
content: [{ type: "text", text: "Session launch cancelled: no prefill message was provided." }],
|
|
420
|
-
details: {
|
|
421
|
-
...detailsBase,
|
|
422
|
-
response: {
|
|
423
|
-
kind: "cancelled",
|
|
424
|
-
comment: "Session launcher had no prefill to queue",
|
|
425
|
-
launchStatus: handoff.status,
|
|
426
|
-
launchReason: handoff.reason,
|
|
427
|
-
} as AskUserResponse,
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (handoff.status !== "failed") {
|
|
433
|
-
// Handoff is scheduled/queued or recoverably editor-prefilled; abort the
|
|
434
|
-
// current turn so the queued command can run without LLM follow-up.
|
|
435
|
-
ctx.abort();
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const launchLabel = launcherResult.action === "compact" ? "compact" : "direct";
|
|
439
|
-
if (handoff.status === "editor_prefill") {
|
|
440
|
-
contentText = `Queued ${launchLabel} handoff fell back to editor prefill: ${handoff.prefill}`;
|
|
441
|
-
} else if (handoff.status === "failed") {
|
|
442
|
-
contentText = `Failed to queue ${launchLabel} handoff: ${handoff.prefill ?? prefill}`;
|
|
443
|
-
} else {
|
|
444
|
-
contentText = `Queued ${launchLabel} handoff: ${handoff.prefill ?? prefill}`;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return {
|
|
448
|
-
content: [{ type: "text", text: contentText }],
|
|
449
|
-
details: {
|
|
450
|
-
...detailsBase,
|
|
451
|
-
response: {
|
|
452
|
-
...response,
|
|
453
|
-
prefill: handoff.prefill ?? prefill,
|
|
454
|
-
launchedWith: launcherResult.action,
|
|
455
|
-
launchStatus: handoff.status,
|
|
456
|
-
launchReason: handoff.reason,
|
|
457
|
-
launchError: handoff.error,
|
|
458
|
-
},
|
|
459
|
-
},
|
|
460
|
-
};
|
|
150
|
+
if (result.type === "action") {
|
|
151
|
+
return runAction(pi, ctx, questions, result, legacy);
|
|
461
152
|
}
|
|
462
153
|
|
|
154
|
+
const answers = expandFileTokens(result.answers, result.attachments);
|
|
155
|
+
const images = imageParts(result.attachments);
|
|
156
|
+
const names = result.attachments.map((a) => (a.kind === "image" ? `[Image #${a.id}]` : `[File #${a.id}]`));
|
|
463
157
|
return {
|
|
464
|
-
content: [{ type: "text", text:
|
|
158
|
+
content: [{ type: "text" as const, text: answersText(questions, answers, images.length ? names : []) }, ...images],
|
|
465
159
|
details: {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
160
|
+
questions,
|
|
161
|
+
answers,
|
|
162
|
+
outcome: "answered",
|
|
163
|
+
...legacy,
|
|
164
|
+
...(result.attachments.length ? { attachments: result.attachments.map(({ id, kind, path, name }) => ({ id, kind, path, name })) } : {}),
|
|
165
|
+
} as AskDetails,
|
|
469
166
|
};
|
|
470
167
|
},
|
|
471
168
|
|
|
472
|
-
renderCall:
|
|
473
|
-
renderResult:
|
|
169
|
+
renderCall: () => new Text("", 0, 0),
|
|
170
|
+
renderResult: (result, _options, theme, _context) => renderAskResult(result.details as AskDetails | undefined, theme as Theme),
|
|
171
|
+
renderShell: "self",
|
|
172
|
+
// The simple render style collapses every tool to one row — the Q→A is
|
|
173
|
+
// the whole point of ask_user, so keep compact `header: answer` rows
|
|
174
|
+
// under the `Asked user` line (utility's simpleResult hook), and hide the
|
|
175
|
+
// `· N output lines` meta (the answers are the output).
|
|
176
|
+
...( {
|
|
177
|
+
simpleMeta: () => "",
|
|
178
|
+
simpleResult: (result: { details?: AskDetails }, theme: Theme) => askRows(result.details, theme),
|
|
179
|
+
} as object),
|
|
474
180
|
});
|
|
475
181
|
}
|
|
182
|
+
|
|
183
|
+
/** Compact Q→A rows for the simple render style's collapsed view. */
|
|
184
|
+
export function askRows(details: AskDetails | undefined, theme: Theme): string[] {
|
|
185
|
+
const questions = details?.questions ?? [];
|
|
186
|
+
if (details === undefined || questions.length === 0) return [];
|
|
187
|
+
if (details.outcome === "cancelled") return [` ${theme.fg("error", "Canceled by the user")}`];
|
|
188
|
+
if (details.outcome === "clarify") return [` ${theme.fg("warning", "Not ready to answer — wants to clarify first")}`];
|
|
189
|
+
if (details.outcome === "unavailable") return [` ${theme.fg("dim", "not shown (no interactive UI or turned off)")}`];
|
|
190
|
+
if (details.outcome === "action") return [` ${theme.fg("muted", "picked an action")}`];
|
|
191
|
+
return questions.map((q, i) => ` ${theme.fg("muted", `${q.header}: ${answerSummary(q, details.answers?.[i])}`)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** end_turn / new_session options (workflow handoffs). */
|
|
195
|
+
async function runAction(pi: ExtensionAPI, ctx: ExtensionContext, questions: AskQuestion[], result: Extract<PanelResult, { type: "action" }>, legacy: Record<string, unknown>) {
|
|
196
|
+
const opt = result.option;
|
|
197
|
+
const details = { questions, answers: result.answers, outcome: "action", ...legacy } as AskDetails;
|
|
198
|
+
if (opt.action === "end_turn") {
|
|
199
|
+
return { content: [{ type: "text" as const, text: `User chose "${opt.label}" and ended the turn.` }], details, terminate: true };
|
|
200
|
+
}
|
|
201
|
+
const prefill = opt.prefill ?? "";
|
|
202
|
+
const launch = await withHerdrBlocked(pi, "ask_user: launch", () => ctx.ui.custom<SessionLauncherResult | null>(renderLauncherUI({ prefill })));
|
|
203
|
+
if (!launch || launch.action === "cancel") {
|
|
204
|
+
return { content: [{ type: "text" as const, text: `User picked "${opt.label}" but cancelled the handoff.` }], details };
|
|
205
|
+
}
|
|
206
|
+
const handoff = launch.action === "compact"
|
|
207
|
+
? queueCompactHandoff({ pi, ctx, prefill, customInstructions: `${COMPACTOR_INSTRUCTION}\nPreparing for new task. Summarize previous work concisely, preserving only what's essential for: ${prefill}` })
|
|
208
|
+
: queueDirectHandoff(pi, ctx, prefill);
|
|
209
|
+
if (handoff.status !== "failed" && handoff.status !== "cancelled") ctx.abort();
|
|
210
|
+
const text = handoff.status === "failed" ? `Failed to queue the ${launch.action} handoff: ${prefill}` : `Queued ${launch.action} handoff: ${handoff.prefill ?? prefill}`;
|
|
211
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* ● Asked user 3 questions ● Asked user Coffee or tea?
|
|
216
|
+
* │ Planet: Jupiter moon └ Tea
|
|
217
|
+
* └ Last book: Dune
|
|
218
|
+
*/
|
|
219
|
+
export function renderAskResult(details: AskDetails | undefined, theme: Theme): Text {
|
|
220
|
+
const t = theme;
|
|
221
|
+
const questions = details?.questions ?? [];
|
|
222
|
+
if (!questions.length) return new Text("", 0, 0);
|
|
223
|
+
const single = questions.length === 1;
|
|
224
|
+
const title = single ? questions[0]!.question.replace(/\s+/g, " ") : `${t.fg("dim", String(questions.length))} questions`;
|
|
225
|
+
const failed = details!.outcome === "cancelled" || details!.outcome === "clarify" || details!.outcome === "unavailable";
|
|
226
|
+
const bullet = failed ? t.fg("error", "●") : t.fg("success", "●");
|
|
227
|
+
const lines = [` ${bullet} Asked user ${title}`];
|
|
228
|
+
const rows: string[] = [];
|
|
229
|
+
if (details!.outcome === "cancelled") rows.push(t.fg("error", "Canceled by the user"));
|
|
230
|
+
else if (details!.outcome === "clarify") rows.push(t.fg("warning", "Not ready to answer — wants to clarify first"));
|
|
231
|
+
else if (details!.outcome === "unavailable") rows.push(t.fg("dim", "not shown (no interactive UI or turned off)"));
|
|
232
|
+
else {
|
|
233
|
+
questions.forEach((q, i) => {
|
|
234
|
+
const a = details!.answers?.[i];
|
|
235
|
+
const text = answerSummary(q, a);
|
|
236
|
+
rows.push(single ? t.fg("muted", text) : t.fg("muted", `${q.header}: ${text}`));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
rows.forEach((r, i) => lines.push(` ${t.fg("borderMuted", i === rows.length - 1 ? "└" : "│")} ${r}`));
|
|
240
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
241
|
+
}
|