@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,757 +1,331 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @pi-unipi/ask-user —
|
|
2
|
+
* @pi-unipi/ask-user — the question panel (Devin-style)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export interface AskUIResult {
|
|
15
|
-
response: AskUserResponse;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Render the ask_user interactive UI.
|
|
4
|
+
* ── Planet ✓ · Foods 3 · Last book ─────────────────────────────
|
|
5
|
+
* Which planet would you most like to visit?
|
|
6
|
+
* ❭ 1 Mars
|
|
7
|
+
* The red planet, dusty and cold
|
|
8
|
+
* 2 Saturn
|
|
9
|
+
* Famous for its rings
|
|
10
|
+
* Other (type your own)
|
|
11
|
+
* ────────────────────────────────────────────────────────────────
|
|
12
|
+
* ↑↓ navigate · ↵ select · ←→ switch question · ? help me out · esc cancel
|
|
13
|
+
* ? Not ready to answer, help me out!
|
|
20
14
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
15
|
+
* Replaces the input area (no overlay; pi restores the editor on close).
|
|
16
|
+
* ↑↓ move · digits pick (single: pick + next; multi: toggle)
|
|
17
|
+
* ␣ toggle (multi)
|
|
18
|
+
* ↵ select and go to the next question; submits on the last one
|
|
19
|
+
* ←→ / tab switch question (on a non-empty "Other" ←→ move the text cursor)
|
|
20
|
+
* Other just type — no Enter needed; paste / drop a file path or Ctrl+V
|
|
21
|
+
* an image to attach it as [Image #N]
|
|
22
|
+
* ? "not ready — help me out" (ends the call; the agent asks what to clarify)
|
|
23
|
+
* esc cancel (the agent's turn stops)
|
|
24
|
+
* Unanswered questions are submitted as skipped — never blocking.
|
|
27
25
|
*/
|
|
28
|
-
export function renderAskUI(params: {
|
|
29
|
-
question: string;
|
|
30
|
-
context?: string;
|
|
31
|
-
options: NormalizedOption[];
|
|
32
|
-
allowMultiple: boolean;
|
|
33
|
-
allowFreeform: boolean;
|
|
34
|
-
timeout?: number;
|
|
35
|
-
}): (
|
|
36
|
-
tui: TUI,
|
|
37
|
-
theme: Theme,
|
|
38
|
-
kb: import("@earendil-works/pi-coding-agent").KeybindingsManager,
|
|
39
|
-
done: (result: AskUIResult | null) => void,
|
|
40
|
-
) => {
|
|
41
|
-
render: (width: number) => string[];
|
|
42
|
-
invalidate: () => void;
|
|
43
|
-
handleInput: (data: string) => void;
|
|
44
|
-
} {
|
|
45
|
-
return (tui, theme, _kb, done) => {
|
|
46
|
-
const { question, context, options, allowMultiple, allowFreeform, timeout } = params;
|
|
47
26
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
27
|
+
import { Input, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
|
|
28
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { formatBytes, stillReferenced, tokenFor, tokenize, type Attachment } from "@pi-unipi/core";
|
|
30
|
+
import { readClipboardImageFile } from "./clipboard.js";
|
|
31
|
+
import { isAnswered, optionValue, type AskOption, type AskQuestion, type QuestionAnswer } from "./questions.js";
|
|
32
|
+
|
|
33
|
+
export type PanelResult =
|
|
34
|
+
| { type: "answered"; answers: QuestionAnswer[]; attachments: Attachment[] }
|
|
35
|
+
| { type: "clarify"; answers: QuestionAnswer[] }
|
|
36
|
+
| { type: "action"; question: number; option: AskOption; answers: QuestionAnswer[] }
|
|
37
|
+
| { type: "cancel" };
|
|
38
|
+
|
|
39
|
+
export interface PanelOptions {
|
|
40
|
+
/** stop: Esc cancels the turn · send: Esc sends what's answered (rest skipped). */
|
|
41
|
+
escape: "stop" | "send";
|
|
42
|
+
/** A digit in a single-choice question also moves on. */
|
|
43
|
+
digitAdvance: boolean;
|
|
44
|
+
/** Show "? Not ready to answer, help me out!". */
|
|
45
|
+
helpLine: boolean;
|
|
46
|
+
}
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
let optionIndex = 0;
|
|
62
|
-
let editMode = false;
|
|
63
|
-
let editTarget: "freeform" | number = "freeform"; // which option is being edited
|
|
64
|
-
// Width-keyed so a terminal resize can never serve stale, over-wide lines.
|
|
65
|
-
const lineCache = new WidthKeyedCache();
|
|
66
|
-
const selected = new Set<string>();
|
|
67
|
-
let customText: string | null = null; // Store custom text (global freeform)
|
|
68
|
-
const optionCustomTexts = new Map<string, string>(); // Per-option custom text for allowCustom
|
|
69
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
-
let remainingMs = timeout;
|
|
48
|
+
export const DEFAULT_PANEL_OPTIONS: PanelOptions = { escape: "stop", digitAdvance: true, helpLine: true };
|
|
71
49
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
selectedText: (t: any) => theme.fg("accent", t),
|
|
78
|
-
description: (t: any) => theme.fg("muted", t),
|
|
79
|
-
scrollInfo: (t: any) => theme.fg("dim", t),
|
|
80
|
-
noMatch: (t: any) => theme.fg("warning", t),
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
const editor = new Editor(tui, editorTheme);
|
|
50
|
+
interface QState {
|
|
51
|
+
cursor: number;
|
|
52
|
+
picked: Set<string>;
|
|
53
|
+
input: Input;
|
|
54
|
+
}
|
|
84
55
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
56
|
+
const OTHER_LABEL = "Other (type your own)";
|
|
57
|
+
const PASTE_START = "\x1b[200~";
|
|
58
|
+
const PASTE_END = "\x1b[201~";
|
|
59
|
+
|
|
60
|
+
export class AskPanel implements Component, Focusable {
|
|
61
|
+
private q = 0;
|
|
62
|
+
private readonly states: QState[];
|
|
63
|
+
private attachments: Attachment[] = [];
|
|
64
|
+
private paste: string | null = null;
|
|
65
|
+
private note: string | null = null;
|
|
66
|
+
private finished = false;
|
|
67
|
+
private _focused = false;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly tui: TUI,
|
|
71
|
+
private readonly theme: Theme,
|
|
72
|
+
private readonly questions: readonly AskQuestion[],
|
|
73
|
+
private readonly done: (result: PanelResult) => void,
|
|
74
|
+
private readonly readClipboard: () => string | undefined = () => readClipboardImageFile(),
|
|
75
|
+
private readonly options: PanelOptions = DEFAULT_PANEL_OPTIONS,
|
|
76
|
+
) {
|
|
77
|
+
this.states = questions.map(() => ({ cursor: 0, picked: new Set<string>(), input: new Input({ prompt: "" }) }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get focused(): boolean {
|
|
81
|
+
return this._focused;
|
|
82
|
+
}
|
|
83
|
+
set focused(v: boolean) {
|
|
84
|
+
this._focused = v;
|
|
85
|
+
this.syncFocus();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── state helpers ──────────────────────────────────────────────────────
|
|
89
|
+
private get question(): AskQuestion {
|
|
90
|
+
return this.questions[this.q]!;
|
|
91
|
+
}
|
|
92
|
+
private get state(): QState {
|
|
93
|
+
return this.states[this.q]!;
|
|
94
|
+
}
|
|
95
|
+
private hasOther(q = this.q): boolean {
|
|
96
|
+
return this.questions[q]!.other !== false;
|
|
97
|
+
}
|
|
98
|
+
private otherIndex(q = this.q): number {
|
|
99
|
+
return this.hasOther(q) ? this.questions[q]!.options.length : -1;
|
|
100
|
+
}
|
|
101
|
+
private rowCount(q = this.q): number {
|
|
102
|
+
return this.questions[q]!.options.length + (this.hasOther(q) ? 1 : 0);
|
|
103
|
+
}
|
|
104
|
+
private onOther(): boolean {
|
|
105
|
+
return this.state.cursor === this.otherIndex();
|
|
106
|
+
}
|
|
107
|
+
private otherText(q = this.q): string {
|
|
108
|
+
return this.states[q]!.input.getValue();
|
|
109
|
+
}
|
|
110
|
+
private syncFocus(): void {
|
|
111
|
+
this.states.forEach((s, i) => (s.input.focused = this._focused && i === this.q && s.cursor === this.otherIndex(i)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
answers(): QuestionAnswer[] {
|
|
115
|
+
return this.questions.map((_, i) => {
|
|
116
|
+
const s = this.states[i]!;
|
|
117
|
+
const text = s.input.getValue().trim();
|
|
118
|
+
const a: QuestionAnswer = { selected: [...s.picked], skipped: false, ...(text ? { custom_text: text } : {}) };
|
|
119
|
+
a.skipped = !isAnswered(a);
|
|
120
|
+
return a;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private finish(result: PanelResult): void {
|
|
125
|
+
if (this.finished) return;
|
|
126
|
+
this.finished = true;
|
|
127
|
+
this.done(result);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private submit(): void {
|
|
131
|
+
const text = this.states.map((s) => s.input.getValue()).join("\n");
|
|
132
|
+
this.finish({ type: "answered", answers: this.answers(), attachments: stillReferenced(text, this.attachments) });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Enter / pick: go to the next question, or submit from the last. */
|
|
136
|
+
private advance(): void {
|
|
137
|
+
if (this.q < this.questions.length - 1) {
|
|
138
|
+
this.q++;
|
|
139
|
+
this.syncFocus();
|
|
140
|
+
} else this.submit();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private switchTo(delta: number): void {
|
|
144
|
+
const next = Math.max(0, Math.min(this.questions.length - 1, this.q + delta));
|
|
145
|
+
if (next !== this.q) {
|
|
146
|
+
this.q = next;
|
|
147
|
+
this.syncFocus();
|
|
88
148
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private pick(index: number, move = true): void {
|
|
152
|
+
const opt = this.question.options[index];
|
|
153
|
+
if (!opt) return;
|
|
154
|
+
const s = this.state;
|
|
155
|
+
if (this.question.multi_select) {
|
|
156
|
+
const v = optionValue(opt);
|
|
157
|
+
if (s.picked.has(v)) s.picked.delete(v);
|
|
158
|
+
else s.picked.add(v);
|
|
159
|
+
return;
|
|
97
160
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (opt.allowCustom) return "input";
|
|
104
|
-
return "select";
|
|
161
|
+
s.picked = new Set([optionValue(opt)]);
|
|
162
|
+
s.input.setValue(""); // single choice: the last action wins
|
|
163
|
+
if (opt.action) {
|
|
164
|
+
this.finish({ type: "action", question: this.q, option: opt, answers: this.answers() });
|
|
165
|
+
return;
|
|
105
166
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
refresh();
|
|
116
|
-
} else {
|
|
117
|
-
// If empty and no previous custom text, uncheck freeform option
|
|
118
|
-
if (!customText) {
|
|
119
|
-
selected.delete("__freeform__");
|
|
120
|
-
}
|
|
121
|
-
editMode = false;
|
|
122
|
-
editor.setText("");
|
|
123
|
-
refresh();
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
// Per-option custom input (allowCustom)
|
|
127
|
-
if (trimmed) {
|
|
128
|
-
setOptionCustomText(editTarget, trimmed);
|
|
129
|
-
editMode = false;
|
|
130
|
-
editor.setText("");
|
|
131
|
-
// Auto-submit in single-select mode
|
|
132
|
-
if (!allowMultiple) {
|
|
133
|
-
cleanup();
|
|
134
|
-
const opt = displayOptions[editTarget];
|
|
135
|
-
done({
|
|
136
|
-
response: {
|
|
137
|
-
kind: "combined",
|
|
138
|
-
selections: [opt.value],
|
|
139
|
-
text: trimmed,
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
refresh();
|
|
145
|
-
} else {
|
|
146
|
-
// Empty: cancel edit mode, keep option selected but without custom text
|
|
147
|
-
editMode = false;
|
|
148
|
-
editor.setText("");
|
|
149
|
-
refresh();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
function cleanup() {
|
|
155
|
-
if (timeoutId) {
|
|
156
|
-
clearTimeout(timeoutId);
|
|
157
|
-
timeoutId = undefined;
|
|
158
|
-
}
|
|
167
|
+
if (move) this.advance();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Text into "Other": file paths become [Image #N] / [File #N] tokens. */
|
|
171
|
+
private insertText(text: string): void {
|
|
172
|
+
if (!this.hasOther()) return;
|
|
173
|
+
if (!this.onOther()) {
|
|
174
|
+
this.state.cursor = this.otherIndex();
|
|
175
|
+
this.syncFocus();
|
|
159
176
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
const { text: tokens, added } = tokenize(text, this.attachments);
|
|
178
|
+
this.attachments.push(...added);
|
|
179
|
+
this.state.input.handleInput(`${PASTE_START}${tokens}${PASTE_END}`);
|
|
180
|
+
if (!this.question.multi_select && this.otherText().trim()) this.state.picked.clear();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private attachClipboardImage(): void {
|
|
184
|
+
const file = this.readClipboard();
|
|
185
|
+
if (!file) {
|
|
186
|
+
this.note = "No image on the clipboard (over SSH, paste a file path instead)";
|
|
187
|
+
return;
|
|
164
188
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
cleanup();
|
|
182
|
-
done({
|
|
183
|
-
response: {
|
|
184
|
-
kind: "timed_out",
|
|
185
|
-
comment: `Timed out after ${timeout}ms`,
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
}, timeout);
|
|
189
|
+
this.insertText(`${this.otherText() && !/\s$/.test(this.otherText()) ? " " : ""}${file} `);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── input ──────────────────────────────────────────────────────────────
|
|
193
|
+
handleInput(data: string): void {
|
|
194
|
+
if (this.finished) return;
|
|
195
|
+
this.note = null;
|
|
196
|
+
// Bracketed paste may arrive in pieces: collect it whole, then tokenize.
|
|
197
|
+
if (this.paste !== null || data.includes(PASTE_START)) {
|
|
198
|
+
this.paste = (this.paste ?? "") + data;
|
|
199
|
+
const end = this.paste.indexOf(PASTE_END);
|
|
200
|
+
if (end < 0) return;
|
|
201
|
+
const body = this.paste.slice(this.paste.indexOf(PASTE_START) + PASTE_START.length, end);
|
|
202
|
+
this.paste = null;
|
|
203
|
+
this.insertText(body);
|
|
204
|
+
return this.tui.requestRender();
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Freeform option: toggle and enter edit mode if checking
|
|
233
|
-
if (selected.has(val)) {
|
|
234
|
-
selected.delete(val);
|
|
235
|
-
customText = null;
|
|
236
|
-
} else {
|
|
237
|
-
selected.add(val);
|
|
238
|
-
if (!customText) {
|
|
239
|
-
editMode = true;
|
|
240
|
-
editTarget = "freeform";
|
|
241
|
-
editor.setText("");
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else if (action === "end_turn") {
|
|
245
|
-
// End turn: immediate
|
|
246
|
-
cleanup();
|
|
247
|
-
done({ response: { kind: "end_turn", selections: [val] } });
|
|
248
|
-
return;
|
|
249
|
-
} else if (action === "new_session") {
|
|
250
|
-
// New session: immediate
|
|
251
|
-
cleanup();
|
|
252
|
-
done({ response: { kind: "new_session", selections: [val], prefill: opt.prefill } });
|
|
253
|
-
return;
|
|
254
|
-
} else if (action === "input") {
|
|
255
|
-
// Input action: toggle and enter edit mode if checking
|
|
256
|
-
if (selected.has(val)) {
|
|
257
|
-
selected.delete(val);
|
|
258
|
-
optionCustomTexts.delete(val);
|
|
259
|
-
} else {
|
|
260
|
-
selected.add(val);
|
|
261
|
-
if (!getOptionCustomText(optionIndex)) {
|
|
262
|
-
editMode = true;
|
|
263
|
-
editTarget = optionIndex;
|
|
264
|
-
editor.setText("");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
// Regular option: toggle
|
|
269
|
-
if (selected.has(val)) {
|
|
270
|
-
selected.delete(val);
|
|
271
|
-
} else {
|
|
272
|
-
selected.add(val);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
refresh();
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Enter: select or submit
|
|
280
|
-
if (matchesKey(data, Key.enter)) {
|
|
281
|
-
const opt = displayOptions[optionIndex];
|
|
282
|
-
const action = getAction(opt);
|
|
283
|
-
|
|
284
|
-
if (action === "freeform") {
|
|
285
|
-
// Freeform option: if already checked with text, submit; otherwise enter edit mode
|
|
286
|
-
if (selected.has(opt.value) && customText) {
|
|
287
|
-
cleanup();
|
|
288
|
-
const regularSelections = Array.from(selected).filter(v => v !== "__freeform__");
|
|
289
|
-
if (regularSelections.length > 0) {
|
|
290
|
-
done({ response: { kind: "combined", selections: regularSelections, text: customText } });
|
|
291
|
-
} else {
|
|
292
|
-
done({ response: { kind: "freeform", text: customText } });
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
selected.add(opt.value);
|
|
296
|
-
editMode = true;
|
|
297
|
-
editTarget = "freeform";
|
|
298
|
-
editor.setText("");
|
|
299
|
-
}
|
|
300
|
-
refresh();
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (action === "end_turn") {
|
|
305
|
-
cleanup();
|
|
306
|
-
done({ response: { kind: "end_turn", selections: [opt.value] } });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (action === "new_session") {
|
|
311
|
-
cleanup();
|
|
312
|
-
done({ response: { kind: "new_session", selections: [opt.value], prefill: opt.prefill } });
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (allowMultiple) {
|
|
317
|
-
// In multi-select, Enter submits current selection
|
|
318
|
-
if (selected.size > 0) {
|
|
319
|
-
cleanup();
|
|
320
|
-
if (customText && selected.has("__freeform__")) {
|
|
321
|
-
const regularSelections = Array.from(selected).filter(v => v !== "__freeform__");
|
|
322
|
-
done({ response: { kind: "combined", selections: regularSelections, text: customText } });
|
|
323
|
-
} else {
|
|
324
|
-
const selections = Array.from(selected);
|
|
325
|
-
const combinedTexts: string[] = [];
|
|
326
|
-
for (const sel of selections) {
|
|
327
|
-
const txt = optionCustomTexts.get(sel);
|
|
328
|
-
if (txt) combinedTexts.push(`${sel}: ${txt}`);
|
|
329
|
-
}
|
|
330
|
-
if (combinedTexts.length > 0) {
|
|
331
|
-
done({ response: { kind: "combined", selections, text: combinedTexts.join("\n") } });
|
|
332
|
-
} else {
|
|
333
|
-
done({ response: { kind: "selection", selections } });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Single-select: check if option has input action
|
|
341
|
-
if (action === "input") {
|
|
342
|
-
const existing = getOptionCustomText(optionIndex);
|
|
343
|
-
if (existing) {
|
|
344
|
-
cleanup();
|
|
345
|
-
done({ response: { kind: "combined", selections: [opt.value], text: existing } });
|
|
346
|
-
} else {
|
|
347
|
-
editMode = true;
|
|
348
|
-
editTarget = optionIndex;
|
|
349
|
-
editor.setText("");
|
|
350
|
-
refresh();
|
|
351
|
-
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Single-select without special action: return immediately
|
|
356
|
-
cleanup();
|
|
357
|
-
done({ response: { kind: "selection", selections: [opt.value] } });
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Escape: cancel
|
|
362
|
-
if (matchesKey(data, Key.escape)) {
|
|
363
|
-
cleanup();
|
|
364
|
-
done(null);
|
|
365
|
-
}
|
|
207
|
+
const s = this.state;
|
|
208
|
+
const other = this.onOther();
|
|
209
|
+
const multi = this.question.multi_select === true;
|
|
210
|
+
|
|
211
|
+
if (matchesKey(data, Key.escape)) return this.options.escape === "send" ? this.submit() : this.finish({ type: "cancel" });
|
|
212
|
+
if (matchesKey(data, Key.up)) {
|
|
213
|
+
s.cursor = Math.max(0, s.cursor - 1);
|
|
214
|
+
} else if (matchesKey(data, Key.down)) {
|
|
215
|
+
s.cursor = Math.min(this.rowCount() - 1, s.cursor + 1);
|
|
216
|
+
} else if (matchesKey(data, Key.tab)) {
|
|
217
|
+
this.switchTo(1);
|
|
218
|
+
} else if (matchesKey(data, "shift+tab")) {
|
|
219
|
+
this.switchTo(-1);
|
|
220
|
+
} else if ((matchesKey(data, Key.left) || matchesKey(data, Key.right)) && !(other && this.otherText())) {
|
|
221
|
+
this.switchTo(matchesKey(data, Key.left) ? -1 : 1);
|
|
222
|
+
} else if (matchesKey(data, Key.enter) || data === "\r") {
|
|
223
|
+
if (other) {
|
|
224
|
+
if (!multi && this.otherText().trim()) s.picked.clear();
|
|
225
|
+
this.advance();
|
|
226
|
+
} else if (multi) {
|
|
227
|
+
const opt = this.question.options[s.cursor];
|
|
228
|
+
if (s.picked.size === 0 && opt) s.picked.add(optionValue(opt));
|
|
229
|
+
this.advance();
|
|
230
|
+
} else this.pick(s.cursor);
|
|
231
|
+
} else if (matchesKey(data, Key.ctrl("v")) || matchesKey(data, Key.alt("v"))) {
|
|
232
|
+
if (this.hasOther()) this.attachClipboardImage();
|
|
233
|
+
} else if (other) {
|
|
234
|
+
s.input.handleInput(data);
|
|
235
|
+
if (!multi && this.otherText().trim()) s.picked.clear();
|
|
236
|
+
} else if (data === " " && multi) {
|
|
237
|
+
this.pick(s.cursor);
|
|
238
|
+
} else if (/^[1-9]$/.test(data)) {
|
|
239
|
+
const i = Number(data) - 1;
|
|
240
|
+
if (i < this.question.options.length) {
|
|
241
|
+
s.cursor = i;
|
|
242
|
+
this.pick(i, this.options.digitAdvance);
|
|
243
|
+
} else if (i === this.otherIndex()) s.cursor = i;
|
|
244
|
+
} else if (data === "?") {
|
|
245
|
+
return this.finish({ type: "clarify", answers: this.answers() });
|
|
246
|
+
} else {
|
|
247
|
+
return; // typing is for "Other" — ignored elsewhere, like Devin
|
|
366
248
|
}
|
|
249
|
+
this.syncFocus();
|
|
250
|
+
this.tui.requestRender();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── render ─────────────────────────────────────────────────────────────
|
|
254
|
+
invalidate(): void {}
|
|
255
|
+
|
|
256
|
+
private chip(i: number): string {
|
|
257
|
+
const t = this.theme;
|
|
258
|
+
const q = this.questions[i]!;
|
|
259
|
+
const s = this.states[i]!;
|
|
260
|
+
const active = i === this.q;
|
|
261
|
+
const label = active ? t.fg("accent", q.header) : t.fg("dim", q.header);
|
|
262
|
+
let mark = "";
|
|
263
|
+
if (q.multi_select && s.picked.size > 0) mark = ` ${active ? t.fg("accent", String(s.picked.size + (s.input.getValue().trim() ? 1 : 0))) : t.fg("dim", String(s.picked.size))}`;
|
|
264
|
+
else if (s.picked.size > 0 || s.input.getValue().trim()) mark = ` ${t.fg("dim", "✓")}`;
|
|
265
|
+
return label + mark;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
render(width: number): string[] {
|
|
269
|
+
// Never wider than the terminal (pi-tui throws on over-wide lines).
|
|
270
|
+
const w = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
271
|
+
const t = this.theme;
|
|
272
|
+
const fit = (line: string) => truncateToWidth(line, w, "");
|
|
273
|
+
const lines: string[] = [];
|
|
367
274
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
for (const line of wrapTextWithAnsi(s, innerWidth)) {
|
|
394
|
-
lines.push(frame(line));
|
|
275
|
+
const chips = this.questions.map((_, i) => this.chip(i)).join(t.fg("dim", " · "));
|
|
276
|
+
const head = `${t.fg("borderMuted", "──")} ${chips} `;
|
|
277
|
+
lines.push(fit(head + t.fg("borderMuted", "─".repeat(Math.max(0, w - visibleWidth(head))))));
|
|
278
|
+
|
|
279
|
+
for (const l of wrapTextWithAnsi(this.question.question, Math.max(1, w - 4))) lines.push(fit(` ${l}`));
|
|
280
|
+
|
|
281
|
+
const s = this.state;
|
|
282
|
+
const other = this.onOther();
|
|
283
|
+
const multi = this.question.multi_select === true;
|
|
284
|
+
const hl = (text: string) => t.bg("selectedBg", text);
|
|
285
|
+
this.question.options.forEach((opt, i) => {
|
|
286
|
+
const at = s.cursor === i;
|
|
287
|
+
const picked = s.picked.has(optionValue(opt));
|
|
288
|
+
const marker = multi
|
|
289
|
+
? picked ? t.fg("accent", "■") : t.fg("dim", "□")
|
|
290
|
+
: at ? t.bold(t.fg("accent", "❭")) : " ";
|
|
291
|
+
const num = other ? "" : `${t.fg("dim", String(i + 1))} `;
|
|
292
|
+
const label = at ? t.bold(t.fg("accent", opt.label)) : picked ? t.bold(opt.label) : opt.label;
|
|
293
|
+
const row = ` ${marker} ${num}${label}`;
|
|
294
|
+
lines.push(fit(at ? hl(row) : row));
|
|
295
|
+
if (opt.description) {
|
|
296
|
+
const indent = other ? " " : " ";
|
|
297
|
+
for (const d of wrapTextWithAnsi(opt.description, Math.max(1, w - indent.length - 1))) {
|
|
298
|
+
const desc = `${indent}${t.fg("muted", d)}`;
|
|
299
|
+
lines.push(fit(at ? hl(desc) : desc));
|
|
395
300
|
}
|
|
396
|
-
};
|
|
397
|
-
const addEmpty = () => lines.push(frame(""));
|
|
398
|
-
|
|
399
|
-
// Top border
|
|
400
|
-
if (bordered) lines.push(border(`╭${safeRepeat("─", innerWidth)}╮`));
|
|
401
|
-
|
|
402
|
-
// Context
|
|
403
|
-
if (context) {
|
|
404
|
-
addWrapped(theme.fg("muted", ` ${context}`));
|
|
405
|
-
addEmpty();
|
|
406
301
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
add(theme.fg("dim", ` ⏱ ${secs}s remaining`));
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
addEmpty();
|
|
423
|
-
if (editMode) {
|
|
424
|
-
add(theme.fg("dim", " Enter to confirm text • Esc to cancel text input"));
|
|
425
|
-
} else if (allowMultiple) {
|
|
426
|
-
const currentOpt = displayOptions[optionIndex];
|
|
427
|
-
const action = currentOpt ? getAction(currentOpt) : "select";
|
|
428
|
-
const base = " ↑↓ navigate • Space toggle • Enter submit • Esc cancel";
|
|
429
|
-
let hint = "";
|
|
430
|
-
if (action === "input" && !optionCustomTexts.get(currentOpt.value)) {
|
|
431
|
-
hint = " • Space to add note";
|
|
432
|
-
} else if (action === "end_turn") {
|
|
433
|
-
hint = " • Space to end turn";
|
|
434
|
-
} else if (action === "new_session") {
|
|
435
|
-
hint = " • Space to start new session";
|
|
436
|
-
}
|
|
437
|
-
add(theme.fg("dim", base + hint));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (this.hasOther()) {
|
|
305
|
+
const text = this.otherText();
|
|
306
|
+
const n = this.otherIndex() + 1;
|
|
307
|
+
if (other) {
|
|
308
|
+
lines.push(fit(hl(` ${t.bold(t.fg("accent", "❭"))} ${t.bold(t.fg("accent", OTHER_LABEL))}`)));
|
|
309
|
+
const field = s.input.render(Math.max(4, w - 6))[0] ?? "";
|
|
310
|
+
lines.push(fit(` ${t.fg("accent", "└")} ${field}`));
|
|
311
|
+
} else if (text) {
|
|
312
|
+
lines.push(fit(` ${multi ? t.fg("accent", "■") : " "} ${t.fg("dim", String(n))} ${OTHER_LABEL}`));
|
|
313
|
+
lines.push(fit(` ${t.fg("dim", "└")} ${t.fg("muted", text)}`));
|
|
438
314
|
} else {
|
|
439
|
-
|
|
440
|
-
const action = currentOpt ? getAction(currentOpt) : "select";
|
|
441
|
-
if (action === "input" && !optionCustomTexts.get(currentOpt.value)) {
|
|
442
|
-
add(theme.fg("dim", " ↑↓ navigate • Enter to add note • Esc cancel"));
|
|
443
|
-
} else if (action === "end_turn") {
|
|
444
|
-
add(theme.fg("dim", " ↑↓ navigate • Enter to end turn • Esc cancel"));
|
|
445
|
-
} else if (action === "new_session") {
|
|
446
|
-
add(theme.fg("dim", " ↑↓ navigate • Enter to start new session • Esc cancel"));
|
|
447
|
-
} else {
|
|
448
|
-
add(theme.fg("dim", " ↑↓ navigate • Enter select • Esc cancel"));
|
|
449
|
-
}
|
|
315
|
+
lines.push(fit(` ${multi ? t.fg("dim", "□") : " "} ${OTHER_LABEL}`));
|
|
450
316
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (bordered) lines.push(border(`╰${safeRepeat("─", innerWidth)}╯`));
|
|
454
|
-
|
|
455
|
-
return lineCache.set(width, lines);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function renderOptions(
|
|
459
|
-
lines: string[],
|
|
460
|
-
add: (s: string) => void,
|
|
461
|
-
theme: Theme,
|
|
462
|
-
width: number,
|
|
463
|
-
) {
|
|
464
|
-
const addWrappedOptionLine = (prefix: string, content: string) => {
|
|
465
|
-
const prefixWidth = visibleWidth(prefix);
|
|
466
|
-
const wrapWidth = contentWidth(width, prefixWidth);
|
|
467
|
-
const continuationPrefix = safeRepeat(" ", prefixWidth);
|
|
468
|
-
const wrapped = wrapTextWithAnsi(content, wrapWidth);
|
|
469
|
-
for (let lineIndex = 0; lineIndex < wrapped.length; lineIndex++) {
|
|
470
|
-
add((lineIndex === 0 ? prefix : continuationPrefix) + wrapped[lineIndex]);
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// Indent descriptions under the option label, but give up the indent
|
|
475
|
-
// entirely when the terminal is too narrow to afford it.
|
|
476
|
-
const descriptionIndent = safeRepeat(" ", width > 10 ? 5 : 0);
|
|
477
|
-
const addWrappedDescription = (description: string) => {
|
|
478
|
-
addWrappedOptionLine(descriptionIndent, theme.fg("muted", description));
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
// Inline editor, inset by 3 columns where there is room for it.
|
|
482
|
-
const editorIndent = safeRepeat(" ", width > 8 ? 3 : 0);
|
|
483
|
-
const addInlineEditor = () => {
|
|
484
|
-
add(`${editorIndent}${theme.fg("muted", "Type your response:")}`);
|
|
485
|
-
for (const line of editor.render(contentWidth(width, visibleWidth(editorIndent) + 1))) {
|
|
486
|
-
add(`${editorIndent}${line}`);
|
|
487
|
-
}
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
for (let i = 0; i < displayOptions.length; i++) {
|
|
491
|
-
const opt = displayOptions[i];
|
|
492
|
-
const isSelected = i === optionIndex;
|
|
493
|
-
const prefix = isSelected ? theme.fg("accent", "> ") : " ";
|
|
494
|
-
|
|
495
|
-
if (opt.isFreeform) {
|
|
496
|
-
// Freeform option: show checkbox like regular option
|
|
497
|
-
const checked = selected.has(opt.value);
|
|
498
|
-
const box = checked ? "✓" : " ";
|
|
499
|
-
const color = checked ? "success" : isSelected ? "accent" : "text";
|
|
500
|
-
|
|
501
|
-
let label = opt.label;
|
|
502
|
-
if (checked && customText) {
|
|
503
|
-
// Show custom text next to label
|
|
504
|
-
label = `${opt.label}: "${customText}"`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
addWrappedOptionLine(
|
|
508
|
-
prefix + theme.fg(color, `[${box}]`) + " ",
|
|
509
|
-
theme.fg(isSelected ? "accent" : "text", label),
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
// Show edit indicator if in edit mode for this option
|
|
513
|
-
if (editMode && editTarget === "freeform" && isSelected) {
|
|
514
|
-
addInlineEditor();
|
|
515
|
-
}
|
|
516
|
-
} else if (allowMultiple) {
|
|
517
|
-
// Multi-select: show checkbox
|
|
518
|
-
const checked = selected.has(opt.value);
|
|
519
|
-
const box = checked ? "✓" : " ";
|
|
520
|
-
const color = checked ? "success" : isSelected ? "accent" : "text";
|
|
521
|
-
|
|
522
|
-
let label = opt.label;
|
|
523
|
-
const optCustom = optionCustomTexts.get(opt.value);
|
|
524
|
-
if (optCustom) {
|
|
525
|
-
label = `${opt.label}: "${optCustom}"`;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
addWrappedOptionLine(
|
|
529
|
-
prefix + theme.fg(color, `[${box}]`) + " ",
|
|
530
|
-
theme.fg(isSelected ? "accent" : "text", label),
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
// Show edit indicator if in edit mode for this option
|
|
534
|
-
if (editMode && editTarget === i && isSelected) {
|
|
535
|
-
addInlineEditor();
|
|
536
|
-
}
|
|
537
|
-
} else {
|
|
538
|
-
// Single-select: option
|
|
539
|
-
let label = opt.label;
|
|
540
|
-
const optCustom = optionCustomTexts.get(opt.value);
|
|
541
|
-
if (optCustom) {
|
|
542
|
-
label = `${opt.label}: "${optCustom}"`;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Show action indicator
|
|
546
|
-
const action = getAction(opt);
|
|
547
|
-
if (action === "input" && !optCustom) {
|
|
548
|
-
label += theme.fg("dim", " (add note)");
|
|
549
|
-
} else if (action === "end_turn") {
|
|
550
|
-
label += theme.fg("dim", " ↵");
|
|
551
|
-
} else if (action === "new_session") {
|
|
552
|
-
label += theme.fg("dim", " ↗");
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
addWrappedOptionLine(
|
|
556
|
-
prefix,
|
|
557
|
-
isSelected
|
|
558
|
-
? theme.fg("accent", label)
|
|
559
|
-
: theme.fg("text", label),
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
// Show edit indicator if in edit mode for this option
|
|
563
|
-
if (editMode && editTarget === i && isSelected) {
|
|
564
|
-
addInlineEditor();
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Description
|
|
569
|
-
if (opt.description) {
|
|
570
|
-
addWrappedDescription(opt.description);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
render,
|
|
577
|
-
invalidate: () => {
|
|
578
|
-
lineCache.clear();
|
|
579
|
-
},
|
|
580
|
-
handleInput,
|
|
581
|
-
};
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Create a renderCall function for the ask_user tool.
|
|
587
|
-
*/
|
|
588
|
-
export function createRenderCall() {
|
|
589
|
-
return (args: Record<string, unknown>, theme: Theme, _context: unknown) => {
|
|
590
|
-
const question = (args.question as string) || "";
|
|
591
|
-
const context = (args.context as string | undefined) || "";
|
|
592
|
-
const options = Array.isArray(args.options)
|
|
593
|
-
? (args.options as Array<Record<string, unknown>>)
|
|
594
|
-
: [];
|
|
595
|
-
const mode = args.allowMultiple ? "multi-select" : "single-select";
|
|
596
|
-
const allowFreeform = args.allowFreeform !== false;
|
|
597
|
-
const count = options.length;
|
|
598
|
-
|
|
599
|
-
const lines: string[] = [];
|
|
600
|
-
lines.push(
|
|
601
|
-
theme.fg("toolTitle", theme.bold("ask_user")) +
|
|
602
|
-
theme.fg("dim", ` (${count} option${count !== 1 ? "s" : ""}, ${mode}${allowFreeform ? ", freeform" : ""})`),
|
|
603
|
-
);
|
|
604
|
-
if (context) {
|
|
605
|
-
lines.push(theme.fg("muted", "Context: ") + theme.fg("text", context));
|
|
606
|
-
}
|
|
607
|
-
lines.push(theme.fg("muted", "Question: ") + theme.fg("text", question));
|
|
608
|
-
|
|
609
|
-
if (count > 0) {
|
|
610
|
-
lines.push(theme.fg("muted", "Options:"));
|
|
611
|
-
options.forEach((option, index) => {
|
|
612
|
-
const label = String(option.label ?? option.value ?? `Option ${index + 1}`);
|
|
613
|
-
const action = typeof option.action === "string" && option.action !== "select"
|
|
614
|
-
? theme.fg("dim", ` [${option.action}]`)
|
|
615
|
-
: "";
|
|
616
|
-
lines.push(
|
|
617
|
-
theme.fg("dim", ` ${index + 1}. `) +
|
|
618
|
-
theme.fg("text", label) +
|
|
619
|
-
action,
|
|
620
|
-
);
|
|
621
|
-
if (typeof option.description === "string" && option.description.trim()) {
|
|
622
|
-
lines.push(theme.fg("muted", ` ${option.description}`));
|
|
623
|
-
}
|
|
624
|
-
if (typeof option.prefill === "string" && option.prefill.trim()) {
|
|
625
|
-
lines.push(theme.fg("dim", ` prefill: ${option.prefill}`));
|
|
626
|
-
}
|
|
627
|
-
});
|
|
317
|
+
const used = stillReferenced(text, this.attachments);
|
|
318
|
+
for (const a of used) lines.push(fit(` ${t.fg("accent", tokenFor(a))} ${t.fg("dim", `${a.name} · ${formatBytes(a.bytes)}`)}`));
|
|
628
319
|
}
|
|
629
320
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
321
|
+
lines.push(t.fg("borderMuted", "─".repeat(w)));
|
|
322
|
+
if (this.note) lines.push(fit(t.fg("warning", this.note)));
|
|
323
|
+
const esc = this.options.escape === "send" ? "esc send" : "esc cancel";
|
|
324
|
+
const keys = ["↑↓ navigate", ...(multi && !other ? ["␣ toggle"] : []), "↵ select", ...(other ? ["ctrl+v image"] : []), ...(this.questions.length > 1 ? ["←→ switch question"] : []), ...(other ? [] : ["? help me out"]), esc];
|
|
325
|
+
lines.push(fit(t.fg("dim", keys.join(" · "))));
|
|
326
|
+
if (this.options.helpLine) lines.push(fit(t.fg("warning", "? Not ready to answer, help me out!")));
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
633
329
|
|
|
634
|
-
|
|
635
|
-
* Create a renderResult function for the ask_user tool.
|
|
636
|
-
*/
|
|
637
|
-
export function createRenderResult() {
|
|
638
|
-
return (result: AgentToolResult<unknown>, options: unknown, theme: Theme, _context: unknown) => {
|
|
639
|
-
const details = result.details as Record<string, unknown> | undefined;
|
|
640
|
-
if (!details) {
|
|
641
|
-
const content = result.content as unknown as Array<Record<string, unknown>> | undefined;
|
|
642
|
-
const text = content?.[0];
|
|
643
|
-
return new Text(text?.type === "text" ? (text.text as string) : "", 0, 0);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const response = (details as { response: AskUserResponse }).response;
|
|
647
|
-
if (!response) {
|
|
648
|
-
return new Text(theme.fg("warning", "No response"), 0, 0);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const renderOptionSummary = (): string[] => {
|
|
652
|
-
const rawOptions = (details as { options?: unknown }).options;
|
|
653
|
-
if (!Array.isArray(rawOptions) || rawOptions.length === 0) return [];
|
|
654
|
-
return rawOptions.map((opt, index) => {
|
|
655
|
-
if (typeof opt === "string") {
|
|
656
|
-
return theme.fg("dim", ` ${index + 1}. `) + theme.fg("text", opt);
|
|
657
|
-
}
|
|
658
|
-
const record = opt as Record<string, unknown>;
|
|
659
|
-
const label = String(record.label ?? record.value ?? `Option ${index + 1}`);
|
|
660
|
-
const value = typeof record.value === "string" && record.value !== label
|
|
661
|
-
? theme.fg("dim", ` (${record.value})`)
|
|
662
|
-
: "";
|
|
663
|
-
const action = typeof record.action === "string" && record.action !== "select"
|
|
664
|
-
? theme.fg("dim", ` [${record.action}]`)
|
|
665
|
-
: "";
|
|
666
|
-
const description = typeof record.description === "string" && record.description.trim()
|
|
667
|
-
? `\n${theme.fg("muted", ` ${record.description}`)}`
|
|
668
|
-
: "";
|
|
669
|
-
return theme.fg("dim", ` ${index + 1}. `) + theme.fg("text", label) + value + action + description;
|
|
670
|
-
});
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
const answerText = (() => {
|
|
674
|
-
switch (response.kind) {
|
|
675
|
-
case "cancelled":
|
|
676
|
-
return theme.fg("warning", "Cancelled");
|
|
677
|
-
case "timed_out":
|
|
678
|
-
return theme.fg("warning", "Timed out");
|
|
679
|
-
case "freeform":
|
|
680
|
-
return theme.fg("success", "✓ ") +
|
|
681
|
-
theme.fg("muted", "(wrote) ") +
|
|
682
|
-
theme.fg("accent", response.text || "");
|
|
683
|
-
case "selection": {
|
|
684
|
-
const selections = response.selections || [];
|
|
685
|
-
const display = selections.length === 1 ? selections[0] : selections.join(", ");
|
|
686
|
-
return theme.fg("success", "✓ ") + theme.fg("accent", display);
|
|
687
|
-
}
|
|
688
|
-
case "combined": {
|
|
689
|
-
const selections = response.selections || [];
|
|
690
|
-
const selDisplay = selections.length === 1
|
|
691
|
-
? selections[0]
|
|
692
|
-
: selections.join(", ");
|
|
693
|
-
return theme.fg("success", "✓ ") +
|
|
694
|
-
theme.fg("accent", selDisplay) +
|
|
695
|
-
theme.fg("muted", " and wrote ") +
|
|
696
|
-
theme.fg("accent", response.text || "");
|
|
697
|
-
}
|
|
698
|
-
case "end_turn":
|
|
699
|
-
return theme.fg("success", "✓ ") + theme.fg("muted", "end turn");
|
|
700
|
-
case "new_session": {
|
|
701
|
-
const prefill = response.prefill || "";
|
|
702
|
-
if (response.launchStatus === "editor_prefill") {
|
|
703
|
-
const label = response.launchedWith === "compact"
|
|
704
|
-
? "⚠ compact editor prefill → "
|
|
705
|
-
: "⚠ direct editor prefill → ";
|
|
706
|
-
return theme.fg("warning", label) + theme.fg("accent", prefill);
|
|
707
|
-
}
|
|
708
|
-
if (response.launchStatus === "failed") {
|
|
709
|
-
const label = response.launchedWith === "compact"
|
|
710
|
-
? "handoff failed (compact) → "
|
|
711
|
-
: "handoff failed (direct) → ";
|
|
712
|
-
return theme.fg("error", label) + theme.fg("accent", prefill);
|
|
713
|
-
}
|
|
714
|
-
if (response.launchedWith === "compact") {
|
|
715
|
-
return theme.fg("success", "✓ queued compact → ") + theme.fg("accent", prefill);
|
|
716
|
-
}
|
|
717
|
-
if (response.launchedWith === "direct") {
|
|
718
|
-
return theme.fg("success", "✓ queued direct → ") + theme.fg("accent", prefill);
|
|
719
|
-
}
|
|
720
|
-
return theme.fg("success", "✓ ") +
|
|
721
|
-
theme.fg("muted", "new session") +
|
|
722
|
-
(prefill ? theme.fg("accent", `: ${prefill}`) : "");
|
|
723
|
-
}
|
|
724
|
-
default:
|
|
725
|
-
return theme.fg("text", JSON.stringify(response));
|
|
726
|
-
}
|
|
727
|
-
})();
|
|
728
|
-
|
|
729
|
-
const expanded = typeof options === "object" && options !== null && "expanded" in options
|
|
730
|
-
? Boolean((options as { expanded?: boolean }).expanded)
|
|
731
|
-
: false;
|
|
732
|
-
if (!expanded) {
|
|
733
|
-
const question = typeof (details as { question?: unknown }).question === "string"
|
|
734
|
-
? (details as { question: string }).question
|
|
735
|
-
: "";
|
|
736
|
-
const expandHint = question
|
|
737
|
-
? theme.fg("dim", " · Ctrl+O question/options")
|
|
738
|
-
: "";
|
|
739
|
-
return new Text(answerText + expandHint, 0, 0);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const lines = [answerText];
|
|
743
|
-
const question = (details as { question?: unknown }).question;
|
|
744
|
-
const context = (details as { context?: unknown }).context;
|
|
745
|
-
if (typeof question === "string" && question.trim()) {
|
|
746
|
-
lines.push(theme.fg("muted", "Question: ") + theme.fg("text", question));
|
|
747
|
-
}
|
|
748
|
-
if (typeof context === "string" && context.trim()) {
|
|
749
|
-
lines.push(theme.fg("muted", "Context: ") + theme.fg("text", context));
|
|
750
|
-
}
|
|
751
|
-
const optionLines = renderOptionSummary();
|
|
752
|
-
if (optionLines.length > 0) {
|
|
753
|
-
lines.push(theme.fg("muted", "Options:"), ...optionLines);
|
|
754
|
-
}
|
|
755
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
756
|
-
};
|
|
330
|
+
dispose(): void {}
|
|
757
331
|
}
|