pi-agent-flow 1.8.40 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -37
- package/agents/audit.md +18 -22
- package/agents/build.md +20 -22
- package/agents/craft.md +20 -27
- package/agents/debug.md +21 -28
- package/agents/ideas.md +18 -101
- package/agents/scout.md +15 -19
- package/dist/batch/batch-bash.d.ts +2 -2
- package/dist/batch/batch-bash.d.ts.map +1 -1
- package/dist/batch/batch-bash.js +3 -3
- package/dist/batch/batch-bash.js.map +1 -1
- package/dist/batch/constants.d.ts +19 -5
- package/dist/batch/constants.d.ts.map +1 -1
- package/dist/batch/constants.js +4 -3
- package/dist/batch/constants.js.map +1 -1
- package/dist/batch/execute.d.ts +0 -1
- package/dist/batch/execute.d.ts.map +1 -1
- package/dist/batch/execute.js +97 -6
- package/dist/batch/execute.js.map +1 -1
- package/dist/batch/fuzzy-edit.d.ts +0 -6
- package/dist/batch/fuzzy-edit.d.ts.map +1 -1
- package/dist/batch/fuzzy-edit.js +1 -1
- package/dist/batch/fuzzy-edit.js.map +1 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +87 -16
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/render.d.ts +0 -1
- package/dist/batch/render.d.ts.map +1 -1
- package/dist/batch/render.js +7 -101
- package/dist/batch/render.js.map +1 -1
- package/dist/batch/summary.d.ts +5 -0
- package/dist/batch/summary.d.ts.map +1 -0
- package/dist/batch/summary.js +101 -0
- package/dist/batch/summary.js.map +1 -0
- package/dist/{config.d.ts → config/config.d.ts} +34 -2
- package/dist/config/config.d.ts.map +1 -0
- package/dist/{config.js → config/config.js} +157 -9
- package/dist/config/config.js.map +1 -0
- package/dist/config/log.d.ts +27 -0
- package/dist/config/log.d.ts.map +1 -0
- package/dist/config/log.js +104 -0
- package/dist/config/log.js.map +1 -0
- package/dist/{settings-resolver.d.ts → config/settings-resolver.d.ts} +9 -2
- package/dist/config/settings-resolver.d.ts.map +1 -0
- package/dist/config/settings-resolver.js +275 -0
- package/dist/config/settings-resolver.js.map +1 -0
- package/dist/core/agents.d.ts.map +1 -0
- package/dist/{agents.js → core/agents.js} +11 -10
- package/dist/core/agents.js.map +1 -0
- package/dist/core/delegation.d.ts +24 -0
- package/dist/core/delegation.d.ts.map +1 -0
- package/dist/core/delegation.js +55 -0
- package/dist/core/delegation.js.map +1 -0
- package/dist/core/depth.d.ts.map +1 -0
- package/dist/{depth.js → core/depth.js} +9 -8
- package/dist/core/depth.js.map +1 -0
- package/dist/{executor.d.ts → core/executor.d.ts} +11 -3
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/{executor.js → core/executor.js} +49 -14
- package/dist/core/executor.js.map +1 -0
- package/dist/{flow.d.ts → core/flow.d.ts} +4 -1
- package/dist/core/flow.d.ts.map +1 -0
- package/dist/{flow.js → core/flow.js} +110 -45
- package/dist/core/flow.js.map +1 -0
- package/dist/{session-mode.d.ts → core/session-mode.d.ts} +2 -1
- package/dist/core/session-mode.d.ts.map +1 -0
- package/dist/{session-mode.js → core/session-mode.js} +1 -1
- package/dist/core/session-mode.js.map +1 -0
- package/dist/core/session-registry.d.ts +16 -0
- package/dist/core/session-registry.d.ts.map +1 -0
- package/dist/core/session-registry.js +30 -0
- package/dist/core/session-registry.js.map +1 -0
- package/dist/core/transitions.d.ts.map +1 -0
- package/dist/{transitions.js → core/transitions.js} +1 -1
- package/dist/core/transitions.js.map +1 -0
- package/dist/flow/command.d.ts +8 -0
- package/dist/flow/command.d.ts.map +1 -0
- package/dist/flow/command.js +189 -0
- package/dist/flow/command.js.map +1 -0
- package/dist/flow/continuation.d.ts +16 -0
- package/dist/flow/continuation.d.ts.map +1 -0
- package/dist/flow/continuation.js +151 -0
- package/dist/flow/continuation.js.map +1 -0
- package/dist/flow/index.d.ts +15 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/flow/index.js +22 -0
- package/dist/flow/index.js.map +1 -0
- package/dist/flow/settings-command.d.ts +51 -0
- package/dist/flow/settings-command.d.ts.map +1 -0
- package/dist/flow/settings-command.js +851 -0
- package/dist/flow/settings-command.js.map +1 -0
- package/dist/flow/store.d.ts +26 -0
- package/dist/flow/store.d.ts.map +1 -0
- package/dist/flow/store.js +158 -0
- package/dist/flow/store.js.map +1 -0
- package/dist/flow/template-strings.d.ts +8 -0
- package/dist/flow/template-strings.d.ts.map +1 -0
- package/dist/flow/template-strings.js +39 -0
- package/dist/flow/template-strings.js.map +1 -0
- package/dist/flow/types.d.ts +55 -0
- package/dist/flow/types.d.ts.map +1 -0
- package/dist/flow/types.js +5 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/flow/warp-command.d.ts +9 -0
- package/dist/flow/warp-command.d.ts.map +1 -0
- package/dist/flow/warp-command.js +405 -0
- package/dist/flow/warp-command.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +103 -29
- package/dist/index.js.map +1 -1
- package/dist/{notify-state.d.ts → notify/notify-state.d.ts} +2 -1
- package/dist/notify/notify-state.d.ts.map +1 -0
- package/dist/notify/notify-state.js.map +1 -0
- package/dist/notify/notify.d.ts.map +1 -0
- package/dist/{notify.js → notify/notify.js} +3 -2
- package/dist/notify/notify.js.map +1 -0
- package/dist/{cli-args.d.ts → snapshot/cli-args.d.ts} +3 -2
- package/dist/snapshot/cli-args.d.ts.map +1 -0
- package/dist/{cli-args.js → snapshot/cli-args.js} +1 -1
- package/dist/snapshot/cli-args.js.map +1 -0
- package/dist/snapshot/index.d.ts +2 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +2 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/{reasoning-strip.d.ts → snapshot/reasoning-strip.d.ts} +0 -4
- package/dist/snapshot/reasoning-strip.d.ts.map +1 -0
- package/dist/{reasoning-strip.js → snapshot/reasoning-strip.js} +2 -2
- package/dist/snapshot/reasoning-strip.js.map +1 -0
- package/dist/snapshot/runner-events.d.ts.map +1 -0
- package/dist/{runner-events.js → snapshot/runner-events.js} +1 -1
- package/dist/snapshot/runner-events.js.map +1 -0
- package/dist/{snapshot.d.ts → snapshot/snapshot.d.ts} +5 -2
- package/dist/snapshot/snapshot.d.ts.map +1 -0
- package/dist/{snapshot.js → snapshot/snapshot.js} +166 -35
- package/dist/snapshot/snapshot.js.map +1 -0
- package/dist/{structured-output.d.ts → snapshot/structured-output.d.ts} +1 -1
- package/dist/snapshot/structured-output.d.ts.map +1 -0
- package/dist/snapshot/structured-output.js.map +1 -0
- package/dist/{flow-prompt.d.ts → steering/flow-prompt.d.ts} +2 -2
- package/dist/steering/flow-prompt.d.ts.map +1 -0
- package/dist/{flow-prompt.js → steering/flow-prompt.js} +1 -1
- package/dist/steering/flow-prompt.js.map +1 -0
- package/dist/{sliding-prompt.d.ts → steering/sliding-prompt.d.ts} +8 -7
- package/dist/steering/sliding-prompt.d.ts.map +1 -0
- package/dist/{sliding-prompt.js → steering/sliding-prompt.js} +15 -64
- package/dist/steering/sliding-prompt.js.map +1 -0
- package/dist/{tool-utils.d.ts → steering/tool-utils.d.ts} +1 -0
- package/dist/steering/tool-utils.d.ts.map +1 -0
- package/dist/{tool-utils.js → steering/tool-utils.js} +10 -3
- package/dist/steering/tool-utils.js.map +1 -0
- package/dist/{ask-user.d.ts → tools/ask-user.d.ts} +3 -15
- package/dist/tools/ask-user.d.ts.map +1 -0
- package/dist/tools/ask-user.js +778 -0
- package/dist/tools/ask-user.js.map +1 -0
- package/dist/{timed-bash.d.ts → tools/timed-bash.d.ts} +2 -7
- package/dist/tools/timed-bash.d.ts.map +1 -0
- package/dist/{timed-bash.js → tools/timed-bash.js} +2 -2
- package/dist/tools/timed-bash.js.map +1 -0
- package/dist/{web-tool.d.ts → tools/web-tool.d.ts} +1 -1
- package/dist/tools/web-tool.d.ts.map +1 -0
- package/dist/{web-tool.js → tools/web-tool.js} +8 -7
- package/dist/tools/web-tool.js.map +1 -0
- package/dist/tui/flow-colors.d.ts +55 -0
- package/dist/tui/flow-colors.d.ts.map +1 -0
- package/dist/tui/flow-colors.js +22 -0
- package/dist/tui/flow-colors.js.map +1 -0
- package/dist/{render-utils.d.ts → tui/render-utils.d.ts} +1 -1
- package/dist/tui/render-utils.d.ts.map +1 -0
- package/dist/{render-utils.js → tui/render-utils.js} +3 -3
- package/dist/tui/render-utils.js.map +1 -0
- package/dist/tui/render.d.ts +21 -0
- package/dist/tui/render.d.ts.map +1 -0
- package/dist/tui/render.js +813 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/scramble/algorithm.d.ts +7 -0
- package/dist/tui/scramble/algorithm.d.ts.map +1 -0
- package/dist/tui/scramble/algorithm.js +227 -0
- package/dist/tui/scramble/algorithm.js.map +1 -0
- package/dist/tui/scramble/constants.d.ts +99 -0
- package/dist/tui/scramble/constants.d.ts.map +1 -0
- package/dist/tui/scramble/constants.js +101 -0
- package/dist/tui/scramble/constants.js.map +1 -0
- package/dist/tui/scramble/index.d.ts +6 -0
- package/dist/tui/scramble/index.d.ts.map +1 -0
- package/dist/tui/scramble/index.js +6 -0
- package/dist/tui/scramble/index.js.map +1 -0
- package/dist/tui/scramble/manager.d.ts +48 -0
- package/dist/tui/scramble/manager.d.ts.map +1 -0
- package/dist/tui/scramble/manager.js +959 -0
- package/dist/tui/scramble/manager.js.map +1 -0
- package/dist/tui/scramble/utils.d.ts +18 -0
- package/dist/tui/scramble/utils.d.ts.map +1 -0
- package/dist/tui/scramble/utils.js +145 -0
- package/dist/tui/scramble/utils.js.map +1 -0
- package/dist/tui/single-select-layout.d.ts +17 -0
- package/dist/tui/single-select-layout.d.ts.map +1 -0
- package/dist/{single-select-layout.js → tui/single-select-layout.js} +8 -25
- package/dist/tui/single-select-layout.js.map +1 -0
- package/dist/types/flow.d.ts +110 -0
- package/dist/types/flow.d.ts.map +1 -0
- package/dist/{types.js → types/flow.js} +3 -54
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/output.d.ts +104 -0
- package/dist/types/output.d.ts.map +1 -0
- package/dist/types/output.js +5 -0
- package/dist/types/output.js.map +1 -0
- package/dist/types/ui.d.ts +24 -0
- package/dist/types/ui.d.ts.map +1 -0
- package/dist/types/ui.js +55 -0
- package/dist/types/ui.js.map +1 -0
- package/package.json +1 -1
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/ask-user.d.ts.map +0 -1
- package/dist/ask-user.js +0 -1405
- package/dist/ask-user.js.map +0 -1
- package/dist/batch.d.ts +0 -12
- package/dist/batch.d.ts.map +0 -1
- package/dist/batch.js +0 -11
- package/dist/batch.js.map +0 -1
- package/dist/cli-args.d.ts.map +0 -1
- package/dist/cli-args.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/depth.d.ts.map +0 -1
- package/dist/depth.js.map +0 -1
- package/dist/executor.d.ts.map +0 -1
- package/dist/executor.js.map +0 -1
- package/dist/flow-prompt.d.ts.map +0 -1
- package/dist/flow-prompt.js.map +0 -1
- package/dist/flow.d.ts.map +0 -1
- package/dist/flow.js.map +0 -1
- package/dist/notify-state.d.ts.map +0 -1
- package/dist/notify-state.js.map +0 -1
- package/dist/notify.d.ts.map +0 -1
- package/dist/notify.js.map +0 -1
- package/dist/reasoning-strip.d.ts.map +0 -1
- package/dist/reasoning-strip.js.map +0 -1
- package/dist/render-utils.d.ts.map +0 -1
- package/dist/render-utils.js.map +0 -1
- package/dist/render.d.ts +0 -24
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -592
- package/dist/render.js.map +0 -1
- package/dist/runner-events.d.ts.map +0 -1
- package/dist/runner-events.js.map +0 -1
- package/dist/scramble.d.ts +0 -183
- package/dist/scramble.d.ts.map +0 -1
- package/dist/scramble.js +0 -2478
- package/dist/scramble.js.map +0 -1
- package/dist/session-mode.d.ts.map +0 -1
- package/dist/session-mode.js.map +0 -1
- package/dist/settings-resolver.d.ts.map +0 -1
- package/dist/settings-resolver.js +0 -148
- package/dist/settings-resolver.js.map +0 -1
- package/dist/single-select-layout.d.ts +0 -20
- package/dist/single-select-layout.d.ts.map +0 -1
- package/dist/single-select-layout.js.map +0 -1
- package/dist/sliding-prompt.d.ts.map +0 -1
- package/dist/sliding-prompt.js.map +0 -1
- package/dist/snapshot.d.ts.map +0 -1
- package/dist/snapshot.js.map +0 -1
- package/dist/spec-mode.d.ts +0 -13
- package/dist/spec-mode.d.ts.map +0 -1
- package/dist/spec-mode.js +0 -90
- package/dist/spec-mode.js.map +0 -1
- package/dist/structured-output.d.ts.map +0 -1
- package/dist/structured-output.js.map +0 -1
- package/dist/timed-bash.d.ts.map +0 -1
- package/dist/timed-bash.js.map +0 -1
- package/dist/tool-utils.d.ts.map +0 -1
- package/dist/tool-utils.js.map +0 -1
- package/dist/transitions.d.ts.map +0 -1
- package/dist/transitions.js.map +0 -1
- package/dist/types.d.ts +0 -224
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/web-tool.d.ts.map +0 -1
- package/dist/web-tool.js.map +0 -1
- /package/dist/{agents.d.ts → core/agents.d.ts} +0 -0
- /package/dist/{depth.d.ts → core/depth.d.ts} +0 -0
- /package/dist/{transitions.d.ts → core/transitions.d.ts} +0 -0
- /package/dist/{notify-state.js → notify/notify-state.js} +0 -0
- /package/dist/{notify.d.ts → notify/notify.d.ts} +0 -0
- /package/dist/{runner-events.d.ts → snapshot/runner-events.d.ts} +0 -0
- /package/dist/{structured-output.js → snapshot/structured-output.js} +0 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask Tool Extension - Interactive question UI for pi-coding-agent
|
|
3
|
+
*
|
|
4
|
+
* Split-pane-only layout: options list (left) + description preview (right).
|
|
5
|
+
* Minimal schema: question + options[{title, description}].
|
|
6
|
+
*/
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import { appendStrategicHintOnce } from "../steering/tool-utils.js";
|
|
9
|
+
import { setPendingDecision } from "../notify/notify-state.js";
|
|
10
|
+
import { scrambleManager, runScrambleTimer } from "../tui/scramble/index.js";
|
|
11
|
+
import { stripAnsi } from "../tui/render-utils.js";
|
|
12
|
+
import { Container, decodeKittyPrintable, fuzzyFilter, Key, matchesKey, Spacer, Text, truncateToWidth, wrapTextWithAnsi, } from "@mariozechner/pi-tui";
|
|
13
|
+
import { renderSingleSelectRows } from "../tui/single-select-layout.js";
|
|
14
|
+
import { loadFlowSettings } from "../config/config.js";
|
|
15
|
+
const ASK_USER_VERSION = "1.8.40";
|
|
16
|
+
/**
|
|
17
|
+
* Emit a flat `{ type: "string", enum: [...] }` JSON Schema instead of the
|
|
18
|
+
* `anyOf`/`oneOf` shape that `Type.Union([Type.Literal()])` produces. Google's
|
|
19
|
+
* function-calling API rejects the union form. Local copy of pi-ai's StringEnum
|
|
20
|
+
* to avoid a peer dependency for one helper.
|
|
21
|
+
*/
|
|
22
|
+
function StringEnum(values, options) {
|
|
23
|
+
return Type.Unsafe({
|
|
24
|
+
type: "string",
|
|
25
|
+
enum: [...values],
|
|
26
|
+
...(options?.description ? { description: options.description } : {}),
|
|
27
|
+
...(options?.default !== undefined ? { default: options.default } : {}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function normalizeOptions(options) {
|
|
31
|
+
return options
|
|
32
|
+
.map((option) => {
|
|
33
|
+
if (typeof option === "string") {
|
|
34
|
+
return { title: option, description: option };
|
|
35
|
+
}
|
|
36
|
+
if (option && typeof option === "object" && typeof option.title === "string") {
|
|
37
|
+
return {
|
|
38
|
+
title: option.title,
|
|
39
|
+
description: option.description || option.title,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
})
|
|
44
|
+
.filter((option) => option !== null);
|
|
45
|
+
}
|
|
46
|
+
function formatOptionsForMessage(options) {
|
|
47
|
+
return options
|
|
48
|
+
.map((option, index) => {
|
|
49
|
+
return `${index + 1}. ${option.title} — ${option.description}`;
|
|
50
|
+
})
|
|
51
|
+
.join("\n");
|
|
52
|
+
}
|
|
53
|
+
function createFreeformResponse(text) {
|
|
54
|
+
const trimmed = text?.trim();
|
|
55
|
+
return trimmed ? { kind: "freeform", text: trimmed } : null;
|
|
56
|
+
}
|
|
57
|
+
function createSelectionResponse(selections) {
|
|
58
|
+
const normalizedSelections = selections.map((selection) => selection.trim()).filter(Boolean);
|
|
59
|
+
if (normalizedSelections.length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
return { kind: "selection", selections: normalizedSelections };
|
|
62
|
+
}
|
|
63
|
+
function formatResponseSummary(response) {
|
|
64
|
+
if (response.kind === "freeform")
|
|
65
|
+
return response.text;
|
|
66
|
+
return response.selections.join(", ");
|
|
67
|
+
}
|
|
68
|
+
function isCancelledInput(value) {
|
|
69
|
+
return value === null || value === undefined;
|
|
70
|
+
}
|
|
71
|
+
function isSelectionResponse(response) {
|
|
72
|
+
return response.kind === "selection";
|
|
73
|
+
}
|
|
74
|
+
function createSelectListTheme(theme) {
|
|
75
|
+
return {
|
|
76
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
77
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
78
|
+
description: (t) => theme.fg("muted", t),
|
|
79
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
80
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const BOX_BORDER_LEFT = "│ ";
|
|
84
|
+
const BOX_BORDER_RIGHT = " │";
|
|
85
|
+
const BOX_BORDER_OVERHEAD = BOX_BORDER_LEFT.length + BOX_BORDER_RIGHT.length;
|
|
86
|
+
class BoxBorderTop {
|
|
87
|
+
color;
|
|
88
|
+
title;
|
|
89
|
+
titleColor;
|
|
90
|
+
constructor(color, title, titleColor) {
|
|
91
|
+
this.color = color;
|
|
92
|
+
this.title = title;
|
|
93
|
+
this.titleColor = titleColor;
|
|
94
|
+
}
|
|
95
|
+
invalidate() { }
|
|
96
|
+
render(width) {
|
|
97
|
+
const paddedWidth = Math.max(0, width - 2);
|
|
98
|
+
let line = this.color("┌" + "─".repeat(paddedWidth) + "┐");
|
|
99
|
+
if (this.title && this.titleColor) {
|
|
100
|
+
const titleWidth = stripAnsi(this.title).length;
|
|
101
|
+
const start = 2;
|
|
102
|
+
const end = start + titleWidth;
|
|
103
|
+
if (end < width - 1) {
|
|
104
|
+
line =
|
|
105
|
+
this.color("┌─") +
|
|
106
|
+
this.titleColor(this.title) +
|
|
107
|
+
this.color("─".repeat(Math.max(0, paddedWidth - titleWidth - 1)) + "┐");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [line];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
class BoxBorderBottom {
|
|
114
|
+
color;
|
|
115
|
+
title;
|
|
116
|
+
titleColor;
|
|
117
|
+
constructor(color, title, titleColor) {
|
|
118
|
+
this.color = color;
|
|
119
|
+
this.title = title;
|
|
120
|
+
this.titleColor = titleColor;
|
|
121
|
+
}
|
|
122
|
+
invalidate() { }
|
|
123
|
+
render(width) {
|
|
124
|
+
const paddedWidth = Math.max(0, width - 2);
|
|
125
|
+
let line = this.color("└" + "─".repeat(paddedWidth) + "┘");
|
|
126
|
+
if (this.title && this.titleColor) {
|
|
127
|
+
const titleWidth = stripAnsi(this.title).length;
|
|
128
|
+
const start = 2;
|
|
129
|
+
const end = start + titleWidth;
|
|
130
|
+
if (end < width - 1) {
|
|
131
|
+
line =
|
|
132
|
+
this.color("└" + "─".repeat(Math.max(0, paddedWidth - titleWidth - 1))) +
|
|
133
|
+
this.titleColor(this.title) +
|
|
134
|
+
this.color("─┘");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [line];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Vim-style aliases for navigating option lists.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
const VIM_SELECT_UP_KEY = Key.ctrl("k");
|
|
144
|
+
const VIM_SELECT_DOWN_KEY = Key.ctrl("j");
|
|
145
|
+
function matchesSelectUp(data, keybindings) {
|
|
146
|
+
return (keybindings.matches(data, "tui.select.up") ||
|
|
147
|
+
matchesKey(data, Key.shift("tab")) ||
|
|
148
|
+
matchesKey(data, VIM_SELECT_UP_KEY));
|
|
149
|
+
}
|
|
150
|
+
function matchesSelectDown(data, keybindings) {
|
|
151
|
+
return (keybindings.matches(data, "tui.select.down") ||
|
|
152
|
+
matchesKey(data, Key.tab) ||
|
|
153
|
+
matchesKey(data, VIM_SELECT_DOWN_KEY));
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Split-pane constants
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
const ASK_OVERLAY_MAX_HEIGHT_RATIO = 0.85;
|
|
159
|
+
// Split-pane layout always renders left list + right preview regardless of terminal width
|
|
160
|
+
const SINGLE_SELECT_SPLIT_PANE_SEPARATOR = " │ ";
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// WrappedSingleSelectList — fuzzy-searchable single-select with split-pane preview
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
class WrappedSingleSelectList {
|
|
165
|
+
options;
|
|
166
|
+
theme;
|
|
167
|
+
keybindings;
|
|
168
|
+
selectedIndex = 0;
|
|
169
|
+
searchQuery = "";
|
|
170
|
+
maxVisibleRows = 12;
|
|
171
|
+
cachedWidth;
|
|
172
|
+
cachedLines;
|
|
173
|
+
onCancel;
|
|
174
|
+
onSubmit;
|
|
175
|
+
constructor(options, theme, keybindings) {
|
|
176
|
+
this.options = options;
|
|
177
|
+
this.theme = theme;
|
|
178
|
+
this.keybindings = keybindings;
|
|
179
|
+
}
|
|
180
|
+
setMaxVisibleRows(rows) {
|
|
181
|
+
const next = Math.max(1, Math.floor(rows));
|
|
182
|
+
if (next !== this.maxVisibleRows) {
|
|
183
|
+
this.maxVisibleRows = next;
|
|
184
|
+
this.invalidate();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
invalidate() {
|
|
188
|
+
this.cachedWidth = undefined;
|
|
189
|
+
this.cachedLines = undefined;
|
|
190
|
+
}
|
|
191
|
+
getFilteredOptions() {
|
|
192
|
+
return fuzzyFilter(this.options, this.searchQuery, (option) => `${option.title} ${option.description}`);
|
|
193
|
+
}
|
|
194
|
+
getItemCount(filteredOptions) {
|
|
195
|
+
return filteredOptions.length;
|
|
196
|
+
}
|
|
197
|
+
setSearchQuery(query) {
|
|
198
|
+
this.searchQuery = query;
|
|
199
|
+
this.selectedIndex = 0;
|
|
200
|
+
this.invalidate();
|
|
201
|
+
}
|
|
202
|
+
popSearchCharacter() {
|
|
203
|
+
if (!this.searchQuery)
|
|
204
|
+
return;
|
|
205
|
+
const characters = [...this.searchQuery];
|
|
206
|
+
characters.pop();
|
|
207
|
+
this.setSearchQuery(characters.join(""));
|
|
208
|
+
}
|
|
209
|
+
getPrintableInput(data) {
|
|
210
|
+
const kittyPrintable = decodeKittyPrintable(data);
|
|
211
|
+
if (kittyPrintable !== undefined)
|
|
212
|
+
return kittyPrintable;
|
|
213
|
+
const characters = [...data];
|
|
214
|
+
if (characters.length !== 1)
|
|
215
|
+
return null;
|
|
216
|
+
const [character] = characters;
|
|
217
|
+
if (!character)
|
|
218
|
+
return null;
|
|
219
|
+
const code = character.charCodeAt(0);
|
|
220
|
+
if (code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return character;
|
|
224
|
+
}
|
|
225
|
+
styleListLine(line, width, isSelected) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (trimmed.startsWith("(")) {
|
|
228
|
+
return truncateToWidth(this.theme.fg("dim", line), width, "");
|
|
229
|
+
}
|
|
230
|
+
if (isSelected) {
|
|
231
|
+
return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
|
|
232
|
+
}
|
|
233
|
+
if (line.startsWith(" ")) {
|
|
234
|
+
return truncateToWidth(this.theme.fg("muted", line), width, "");
|
|
235
|
+
}
|
|
236
|
+
if (line.startsWith("▶")) {
|
|
237
|
+
return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
|
|
238
|
+
}
|
|
239
|
+
return truncateToWidth(this.theme.fg("text", line), width, "");
|
|
240
|
+
}
|
|
241
|
+
getSplitPaneWidths(width) {
|
|
242
|
+
const availableWidth = Math.max(1, width - SINGLE_SELECT_SPLIT_PANE_SEPARATOR.length);
|
|
243
|
+
const preferredLeftWidth = Math.floor(availableWidth * 0.42);
|
|
244
|
+
const left = Math.max(1, Math.min(preferredLeftWidth, availableWidth - 1));
|
|
245
|
+
const right = Math.max(1, availableWidth - left);
|
|
246
|
+
return { left, right };
|
|
247
|
+
}
|
|
248
|
+
buildListLines(width, filteredOptions) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
const count = this.getItemCount(filteredOptions);
|
|
251
|
+
const searchValue = this.searchQuery ? this.theme.fg("text", this.searchQuery) : this.theme.fg("dim", "type to filter");
|
|
252
|
+
lines.push(truncateToWidth(`${this.theme.fg("accent", "Filter:")} ${searchValue}`, width, ""));
|
|
253
|
+
if (this.searchQuery && filteredOptions.length === 0) {
|
|
254
|
+
lines.push(truncateToWidth(this.theme.fg("warning", "No matching options"), width, ""));
|
|
255
|
+
}
|
|
256
|
+
if (count === 0) {
|
|
257
|
+
if (!this.searchQuery) {
|
|
258
|
+
lines.push(truncateToWidth(this.theme.fg("warning", "No options"), width, ""));
|
|
259
|
+
}
|
|
260
|
+
return lines.slice(0, this.maxVisibleRows);
|
|
261
|
+
}
|
|
262
|
+
const maxRows = Math.max(1, this.maxVisibleRows - lines.length);
|
|
263
|
+
const optionRows = renderSingleSelectRows({
|
|
264
|
+
options: filteredOptions,
|
|
265
|
+
selectedIndex: this.selectedIndex,
|
|
266
|
+
width,
|
|
267
|
+
maxRows,
|
|
268
|
+
});
|
|
269
|
+
const optionLines = optionRows.map((row) => this.styleListLine(row.line, width, row.selected));
|
|
270
|
+
lines.push(...optionLines);
|
|
271
|
+
return lines.slice(0, this.maxVisibleRows);
|
|
272
|
+
}
|
|
273
|
+
buildPreviewLines(width, filteredOptions, maxLines) {
|
|
274
|
+
if (maxLines <= 0)
|
|
275
|
+
return [];
|
|
276
|
+
let text = "";
|
|
277
|
+
const selected = filteredOptions[this.selectedIndex];
|
|
278
|
+
if (!selected) {
|
|
279
|
+
text += "*No option selected*\n";
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
text += `## ${selected.title}\n\n`;
|
|
283
|
+
if (selected.description?.trim()) {
|
|
284
|
+
text += `${selected.description}\n`;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
text += "*No additional details provided for this option.*\n";
|
|
288
|
+
}
|
|
289
|
+
text += `\n---\n\nPress Enter to select this option.\n`;
|
|
290
|
+
if (this.searchQuery) {
|
|
291
|
+
text += `\n> Filter: ${this.searchQuery}\n`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const lines = [];
|
|
295
|
+
for (const line of wrapTextWithAnsi(text.trim(), Math.max(10, width))) {
|
|
296
|
+
lines.push(truncateToWidth(line, width, ""));
|
|
297
|
+
}
|
|
298
|
+
while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") {
|
|
299
|
+
lines.pop();
|
|
300
|
+
}
|
|
301
|
+
if (lines.length <= maxLines)
|
|
302
|
+
return lines;
|
|
303
|
+
if (maxLines === 1)
|
|
304
|
+
return [truncateToWidth(this.theme.fg("dim", "…"), width, "")];
|
|
305
|
+
const visibleLines = lines.slice(0, maxLines - 1);
|
|
306
|
+
visibleLines.push(truncateToWidth(this.theme.fg("dim", "…"), width, ""));
|
|
307
|
+
return visibleLines;
|
|
308
|
+
}
|
|
309
|
+
handleInput(data) {
|
|
310
|
+
if (this.searchQuery && matchesKey(data, Key.escape)) {
|
|
311
|
+
this.setSearchQuery("");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
315
|
+
this.onCancel?.();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const filteredOptions = this.getFilteredOptions();
|
|
319
|
+
const count = this.getItemCount(filteredOptions);
|
|
320
|
+
if (matchesSelectUp(data, this.keybindings) && count > 0) {
|
|
321
|
+
this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
|
|
322
|
+
this.invalidate();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (matchesSelectDown(data, this.keybindings) && count > 0) {
|
|
326
|
+
this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1;
|
|
327
|
+
this.invalidate();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const numMatch = data.match(/^[1-9]$/);
|
|
331
|
+
if (numMatch && filteredOptions.length > 0) {
|
|
332
|
+
const idx = Number.parseInt(numMatch[0], 10) - 1;
|
|
333
|
+
if (idx >= 0 && idx < filteredOptions.length) {
|
|
334
|
+
this.selectedIndex = idx;
|
|
335
|
+
this.invalidate();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (this.keybindings.matches(data, "tui.select.confirm") && count > 0) {
|
|
340
|
+
const result = filteredOptions[this.selectedIndex]?.title;
|
|
341
|
+
if (result)
|
|
342
|
+
this.onSubmit?.(result);
|
|
343
|
+
else
|
|
344
|
+
this.onCancel?.();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (this.keybindings.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, Key.backspace)) {
|
|
348
|
+
this.popSearchCharacter();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const printableInput = this.getPrintableInput(data);
|
|
352
|
+
if (printableInput) {
|
|
353
|
+
this.setSearchQuery(this.searchQuery + printableInput);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
render(width) {
|
|
357
|
+
if (this.cachedLines && this.cachedWidth === width) {
|
|
358
|
+
return this.cachedLines;
|
|
359
|
+
}
|
|
360
|
+
const filteredOptions = this.getFilteredOptions();
|
|
361
|
+
const count = this.getItemCount(filteredOptions);
|
|
362
|
+
this.selectedIndex = count > 0 ? Math.max(0, Math.min(this.selectedIndex, count - 1)) : 0;
|
|
363
|
+
const splitPane = this.getSplitPaneWidths(width);
|
|
364
|
+
const listLines = this.buildListLines(splitPane.left, filteredOptions);
|
|
365
|
+
const previewLines = this.buildPreviewLines(splitPane.right, filteredOptions, this.maxVisibleRows);
|
|
366
|
+
const rowCount = Math.min(this.maxVisibleRows, Math.max(listLines.length, previewLines.length));
|
|
367
|
+
const separator = this.theme.fg("dim", SINGLE_SELECT_SPLIT_PANE_SEPARATOR);
|
|
368
|
+
const lines = Array.from({ length: rowCount }, (_, index) => {
|
|
369
|
+
const left = truncateToWidth(listLines[index] ?? "", splitPane.left, "", true);
|
|
370
|
+
const right = truncateToWidth(previewLines[index] ?? "", splitPane.right, "");
|
|
371
|
+
return `${left}${separator}${right}`;
|
|
372
|
+
});
|
|
373
|
+
this.cachedWidth = width;
|
|
374
|
+
this.cachedLines = lines;
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// AskComponent — root container with box border and single-select split-pane
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
class AskComponent extends Container {
|
|
382
|
+
question;
|
|
383
|
+
options;
|
|
384
|
+
tui;
|
|
385
|
+
theme;
|
|
386
|
+
keybindings;
|
|
387
|
+
onDone;
|
|
388
|
+
// Static layout components
|
|
389
|
+
titleText;
|
|
390
|
+
questionText;
|
|
391
|
+
modeContainer;
|
|
392
|
+
helpText;
|
|
393
|
+
// Mode component
|
|
394
|
+
singleSelectList;
|
|
395
|
+
// Countdown timer
|
|
396
|
+
timerEnabled;
|
|
397
|
+
timerSeconds;
|
|
398
|
+
timerInterval;
|
|
399
|
+
timerStartMs;
|
|
400
|
+
constructor(question, options, tui, theme, keybindings, onDone) {
|
|
401
|
+
super();
|
|
402
|
+
this.question = question;
|
|
403
|
+
this.options = options;
|
|
404
|
+
this.tui = tui;
|
|
405
|
+
this.theme = theme;
|
|
406
|
+
this.keybindings = keybindings;
|
|
407
|
+
this.onDone = onDone;
|
|
408
|
+
const settings = loadFlowSettings(process.cwd());
|
|
409
|
+
this.timerEnabled = settings.askUser?.enabled ?? true;
|
|
410
|
+
this.timerSeconds = settings.askUser?.timeout ?? 300;
|
|
411
|
+
this.timerInterval = undefined;
|
|
412
|
+
this.timerStartMs = Date.now();
|
|
413
|
+
if (this.timerEnabled && this.timerSeconds > 0) {
|
|
414
|
+
this.timerInterval = setInterval(() => {
|
|
415
|
+
this.timerSeconds--;
|
|
416
|
+
if (this.timerSeconds <= 0) {
|
|
417
|
+
this.finish(null);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
this.tui.requestRender();
|
|
421
|
+
}
|
|
422
|
+
}, 1000);
|
|
423
|
+
}
|
|
424
|
+
this.addChild(new BoxBorderTop((s) => theme.fg("accent", s), "ask_user", (s) => theme.fg("dim", theme.bold(s))));
|
|
425
|
+
this.addChild(new Spacer(1));
|
|
426
|
+
this.titleText = new Text("", 1, 0);
|
|
427
|
+
this.addChild(this.titleText);
|
|
428
|
+
this.addChild(new Spacer(1));
|
|
429
|
+
this.questionText = new Text("", 1, 0);
|
|
430
|
+
this.addChild(this.questionText);
|
|
431
|
+
this.addChild(new Spacer(1));
|
|
432
|
+
this.modeContainer = new Container();
|
|
433
|
+
this.addChild(this.modeContainer);
|
|
434
|
+
this.addChild(new Spacer(1));
|
|
435
|
+
this.helpText = new Text("", 1, 0);
|
|
436
|
+
this.addChild(this.helpText);
|
|
437
|
+
this.addChild(new Spacer(1));
|
|
438
|
+
this.addChild(new BoxBorderBottom((s) => theme.fg("accent", s), `v${ASK_USER_VERSION}`, (s) => theme.fg("dim", s)));
|
|
439
|
+
this.updateStaticText();
|
|
440
|
+
this.showSelectMode();
|
|
441
|
+
}
|
|
442
|
+
invalidate() {
|
|
443
|
+
super.invalidate();
|
|
444
|
+
this.updateStaticText();
|
|
445
|
+
this.updateHelpText();
|
|
446
|
+
}
|
|
447
|
+
render(width) {
|
|
448
|
+
const innerWidth = Math.max(1, width - BOX_BORDER_OVERHEAD);
|
|
449
|
+
const overlayMaxHeight = Math.max(12, Math.floor(this.tui.terminal.rows * ASK_OVERLAY_MAX_HEIGHT_RATIO));
|
|
450
|
+
const staticLines = this.countStaticLines(innerWidth);
|
|
451
|
+
const availableOptionRows = Math.max(4, overlayMaxHeight - staticLines);
|
|
452
|
+
this.ensureSingleSelectList().setMaxVisibleRows(availableOptionRows);
|
|
453
|
+
const rawLines = super.render(innerWidth);
|
|
454
|
+
const borderColor = (s) => this.theme.fg("accent", s);
|
|
455
|
+
const titleColor = (s) => this.theme.fg("dim", this.theme.bold(s));
|
|
456
|
+
return rawLines.map((line, index) => {
|
|
457
|
+
if (index === 0 || index === rawLines.length - 1) {
|
|
458
|
+
if (index === 0) {
|
|
459
|
+
return new BoxBorderTop(borderColor, "ask_user", titleColor).render(width)[0];
|
|
460
|
+
}
|
|
461
|
+
return new BoxBorderBottom(borderColor, this.getBottomTitle(), (s) => this.theme.fg("dim", s)).render(width)[0];
|
|
462
|
+
}
|
|
463
|
+
const padded = truncateToWidth(line, innerWidth, "", true);
|
|
464
|
+
return `${borderColor(BOX_BORDER_LEFT)}${padded}${borderColor(BOX_BORDER_RIGHT)}`;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
countWrappedLines(text, width) {
|
|
468
|
+
return Math.max(1, wrapTextWithAnsi(text, Math.max(10, width - 2)).length);
|
|
469
|
+
}
|
|
470
|
+
countStaticLines(width) {
|
|
471
|
+
const titleLines = 1;
|
|
472
|
+
const questionLines = this.countWrappedLines(this.question, width);
|
|
473
|
+
const helpLines = 1;
|
|
474
|
+
const borderLines = 2;
|
|
475
|
+
const spacerLines = 5;
|
|
476
|
+
return borderLines + spacerLines + titleLines + questionLines + helpLines;
|
|
477
|
+
}
|
|
478
|
+
finish(result) {
|
|
479
|
+
this.onDone(result);
|
|
480
|
+
}
|
|
481
|
+
updateStaticText() {
|
|
482
|
+
const theme = this.theme;
|
|
483
|
+
this.titleText.setText(theme.fg("accent", theme.bold("Question")));
|
|
484
|
+
this.questionText.setText(theme.fg("text", theme.bold(this.question)));
|
|
485
|
+
}
|
|
486
|
+
updateHelpText() {
|
|
487
|
+
const theme = this.theme;
|
|
488
|
+
const alternateCancelKeys = this.keybindings
|
|
489
|
+
.getKeys("tui.select.cancel")
|
|
490
|
+
.filter((key) => key !== "escape" && key !== "esc");
|
|
491
|
+
const hints = [
|
|
492
|
+
literalHint(theme, "type", "filter"),
|
|
493
|
+
keybindingHint(theme, this.keybindings, "tui.editor.deleteCharBackward", "erase"),
|
|
494
|
+
literalHint(theme, "▲▼", "navigate"),
|
|
495
|
+
keybindingHint(theme, this.keybindings, "tui.select.confirm", "select"),
|
|
496
|
+
literalHint(theme, "esc", "clear/cancel"),
|
|
497
|
+
alternateCancelKeys.length > 0
|
|
498
|
+
? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel")
|
|
499
|
+
: null,
|
|
500
|
+
]
|
|
501
|
+
.filter((hint) => !!hint)
|
|
502
|
+
.join(" • ");
|
|
503
|
+
this.helpText.setText(theme.fg("dim", hints));
|
|
504
|
+
}
|
|
505
|
+
ensureSingleSelectList() {
|
|
506
|
+
if (this.singleSelectList)
|
|
507
|
+
return this.singleSelectList;
|
|
508
|
+
const list = new WrappedSingleSelectList(this.options, this.theme, this.keybindings);
|
|
509
|
+
list.onSubmit = (result) => this.finish(createSelectionResponse([result]));
|
|
510
|
+
list.onCancel = () => this.finish(null);
|
|
511
|
+
this.singleSelectList = list;
|
|
512
|
+
return list;
|
|
513
|
+
}
|
|
514
|
+
showSelectMode() {
|
|
515
|
+
this.modeContainer.clear();
|
|
516
|
+
this.modeContainer.addChild(this.ensureSingleSelectList());
|
|
517
|
+
this.updateHelpText();
|
|
518
|
+
this.invalidate();
|
|
519
|
+
this.tui.requestRender();
|
|
520
|
+
}
|
|
521
|
+
destroy() {
|
|
522
|
+
if (this.timerInterval !== undefined) {
|
|
523
|
+
clearInterval(this.timerInterval);
|
|
524
|
+
this.timerInterval = undefined;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
getBottomTitle() {
|
|
528
|
+
if (this.timerEnabled && this.timerSeconds > 0) {
|
|
529
|
+
const mins = Math.floor(this.timerSeconds / 60);
|
|
530
|
+
const secs = this.timerSeconds % 60;
|
|
531
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")} · v${ASK_USER_VERSION}`;
|
|
532
|
+
}
|
|
533
|
+
return `v${ASK_USER_VERSION}`;
|
|
534
|
+
}
|
|
535
|
+
handleInput(data) {
|
|
536
|
+
this.ensureSingleSelectList().handleInput?.(data);
|
|
537
|
+
this.tui.requestRender();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// RPC/headless fallback: use dialog methods (select/input) instead of the rich TUI overlay.
|
|
542
|
+
// ctx.ui.custom() returns undefined in RPC mode, so we degrade gracefully.
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
async function askViaDialogs(ui, question, options) {
|
|
545
|
+
if (options.length === 0) {
|
|
546
|
+
const answer = await ui.input(question, "Type your answer...");
|
|
547
|
+
if (isCancelledInput(answer))
|
|
548
|
+
return null;
|
|
549
|
+
return createFreeformResponse(answer);
|
|
550
|
+
}
|
|
551
|
+
const selectOptions = options.map((o) => o.title);
|
|
552
|
+
const selected = await ui.select(question, selectOptions);
|
|
553
|
+
if (isCancelledInput(selected))
|
|
554
|
+
return null;
|
|
555
|
+
return createSelectionResponse([selected]);
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Tool factory
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
export function createAskUserTool() {
|
|
561
|
+
return {
|
|
562
|
+
name: "ask_user",
|
|
563
|
+
label: "Ask User",
|
|
564
|
+
description: "Ask the user a focused question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. When presenting options, mark your recommended choice with [preferred] and place it first.",
|
|
565
|
+
promptSnippet: "Ask the user one focused question with optional multiple-choice answers to gather information interactively",
|
|
566
|
+
promptGuidelines: [
|
|
567
|
+
"Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
|
|
568
|
+
"Ask exactly one focused question per ask_user call.",
|
|
569
|
+
"Do not combine multiple numbered, multipart, or unrelated questions into one ask_user prompt.",
|
|
570
|
+
],
|
|
571
|
+
parameters: Type.Object({
|
|
572
|
+
question: Type.String({ description: "The question to ask the user" }),
|
|
573
|
+
options: Type.Optional(Type.Array(Type.Object({
|
|
574
|
+
title: Type.String({ description: "Short title for this option" }),
|
|
575
|
+
description: Type.String({ description: "Longer description explaining this option" }),
|
|
576
|
+
}), { description: "List of options for the user to choose from" })),
|
|
577
|
+
}),
|
|
578
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
579
|
+
setPendingDecision();
|
|
580
|
+
if (signal?.aborted) {
|
|
581
|
+
return {
|
|
582
|
+
content: [{ type: "text", text: "Cancelled" }],
|
|
583
|
+
details: { question: params.question, options: [], response: null, cancelled: true },
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const { question, options: rawOptions = [] } = params;
|
|
587
|
+
const options = normalizeOptions(rawOptions);
|
|
588
|
+
if (!ctx.hasUI || !ctx.ui) {
|
|
589
|
+
const optionText = options.length > 0 ? `\n\nOptions:\n${formatOptionsForMessage(options)}` : "";
|
|
590
|
+
return {
|
|
591
|
+
content: [
|
|
592
|
+
{
|
|
593
|
+
type: "text",
|
|
594
|
+
text: `Ask requires interactive mode. Please answer:\n\n${question}${optionText}`,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
isError: true,
|
|
598
|
+
details: { question, options, response: null, cancelled: true },
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (options.length === 0) {
|
|
602
|
+
const answer = await ctx.ui.input(question, "Type your answer...");
|
|
603
|
+
const response = createFreeformResponse(answer);
|
|
604
|
+
if (!response) {
|
|
605
|
+
return {
|
|
606
|
+
content: [{ type: "text", text: "User cancelled the question" }],
|
|
607
|
+
details: { question, options, response: null, cancelled: true },
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const _result0 = {
|
|
611
|
+
content: [{ type: "text", text: `User answered: ${formatResponseSummary(response)}` }],
|
|
612
|
+
details: { question, options, response, cancelled: false },
|
|
613
|
+
};
|
|
614
|
+
appendStrategicHintOnce(_result0);
|
|
615
|
+
return _result0;
|
|
616
|
+
}
|
|
617
|
+
onUpdate?.({
|
|
618
|
+
content: [{ type: "text", text: "Waiting for user input..." }],
|
|
619
|
+
details: { question, options, response: null, cancelled: false },
|
|
620
|
+
});
|
|
621
|
+
let result;
|
|
622
|
+
try {
|
|
623
|
+
const customFactory = (tui, theme, keybindings, done) => {
|
|
624
|
+
let abortListener;
|
|
625
|
+
let component;
|
|
626
|
+
let doneCalled = false;
|
|
627
|
+
const cleanup = () => {
|
|
628
|
+
if (signal && abortListener) {
|
|
629
|
+
signal.removeEventListener("abort", abortListener);
|
|
630
|
+
abortListener = undefined;
|
|
631
|
+
}
|
|
632
|
+
component?.destroy?.();
|
|
633
|
+
};
|
|
634
|
+
const wrappedDone = (result) => {
|
|
635
|
+
if (doneCalled)
|
|
636
|
+
return;
|
|
637
|
+
doneCalled = true;
|
|
638
|
+
cleanup();
|
|
639
|
+
done(result);
|
|
640
|
+
};
|
|
641
|
+
if (signal) {
|
|
642
|
+
abortListener = () => wrappedDone(null);
|
|
643
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
644
|
+
}
|
|
645
|
+
component = new AskComponent(question, options, tui, theme, keybindings, wrappedDone);
|
|
646
|
+
return component;
|
|
647
|
+
};
|
|
648
|
+
const customResult = await ctx.ui.custom(customFactory, {
|
|
649
|
+
overlay: true,
|
|
650
|
+
overlayOptions: {
|
|
651
|
+
anchor: "center",
|
|
652
|
+
width: "92%",
|
|
653
|
+
minWidth: 40,
|
|
654
|
+
maxHeight: "85%",
|
|
655
|
+
margin: 1,
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
if (customResult !== undefined) {
|
|
659
|
+
result = customResult;
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
result = await askViaDialogs(ctx.ui, question, options);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
const message = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
|
|
667
|
+
return {
|
|
668
|
+
content: [{ type: "text", text: `Ask tool failed: ${message}` }],
|
|
669
|
+
isError: true,
|
|
670
|
+
details: { error: message },
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (result === null) {
|
|
674
|
+
return {
|
|
675
|
+
content: [{ type: "text", text: "User cancelled the question" }],
|
|
676
|
+
details: { question, options, response: null, cancelled: true },
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
content: [{ type: "text", text: `User answered: ${formatResponseSummary(result)}` }],
|
|
681
|
+
details: {
|
|
682
|
+
question,
|
|
683
|
+
options,
|
|
684
|
+
response: result,
|
|
685
|
+
cancelled: false,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
renderCall(args, theme) {
|
|
690
|
+
const question = args.question || "";
|
|
691
|
+
const rawOptions = Array.isArray(args.options) ? args.options : [];
|
|
692
|
+
let text = theme.fg("toolTitle", theme.bold("ask_user "));
|
|
693
|
+
text += theme.fg("muted", question);
|
|
694
|
+
if (rawOptions.length > 0) {
|
|
695
|
+
const labels = rawOptions.map((o) => typeof o === "string" ? o : o?.title ?? "");
|
|
696
|
+
text += "\n" + theme.fg("dim", ` ${rawOptions.length} option(s): ${labels.join(", ")}`);
|
|
697
|
+
}
|
|
698
|
+
return new Text(text, 0, 0);
|
|
699
|
+
},
|
|
700
|
+
renderResult(result, options, theme, args) {
|
|
701
|
+
const details = result.details;
|
|
702
|
+
const canAnimate = !!args?.invalidate && !!args?.state;
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
const id = args?.toolCallId || args?.id || "ask_user";
|
|
705
|
+
if (details?.error) {
|
|
706
|
+
const line = theme.fg("error", `✖ ${details.error}`);
|
|
707
|
+
if (!canAnimate)
|
|
708
|
+
return new Text(scrambleManager.renderStatic(line), 0, 0);
|
|
709
|
+
const scrambled = scrambleManager.updateText(id, "result", stripAnsi(line), now, false).content;
|
|
710
|
+
runScrambleTimer(args, id);
|
|
711
|
+
return new Text(scrambled, 0, 0);
|
|
712
|
+
}
|
|
713
|
+
if (options.isPartial) {
|
|
714
|
+
const waitingText = result.content
|
|
715
|
+
?.filter((part) => part?.type === "text")
|
|
716
|
+
.map((part) => part.text ?? "")
|
|
717
|
+
.join("\n")
|
|
718
|
+
.trim() || "Waiting for user input...";
|
|
719
|
+
const line = theme.fg("muted", waitingText);
|
|
720
|
+
if (!canAnimate)
|
|
721
|
+
return new Text(scrambleManager.renderStatic(line), 0, 0);
|
|
722
|
+
const scrambled = scrambleManager.updateText(id, "result", stripAnsi(line), now, false).content;
|
|
723
|
+
runScrambleTimer(args, id);
|
|
724
|
+
return new Text(scrambled, 0, 0);
|
|
725
|
+
}
|
|
726
|
+
if (!details || details.cancelled || !details.response) {
|
|
727
|
+
const line = theme.fg("warning", "Cancelled");
|
|
728
|
+
if (!canAnimate)
|
|
729
|
+
return new Text(scrambleManager.renderStatic(line), 0, 0);
|
|
730
|
+
const scrambled = scrambleManager.updateText(id, "result", stripAnsi(line), now, false).content;
|
|
731
|
+
runScrambleTimer(args, id);
|
|
732
|
+
return new Text(scrambled, 0, 0);
|
|
733
|
+
}
|
|
734
|
+
const response = details.response;
|
|
735
|
+
let text = theme.fg("success", "✔ ");
|
|
736
|
+
if (response.kind === "freeform") {
|
|
737
|
+
text += theme.fg("muted", "(wrote) ");
|
|
738
|
+
}
|
|
739
|
+
text += theme.fg("accent", formatResponseSummary(response));
|
|
740
|
+
if (options.expanded) {
|
|
741
|
+
text += "\n" + theme.fg("dim", `Q: ${details.question}`);
|
|
742
|
+
if (isSelectionResponse(response) && details.options.length > 0) {
|
|
743
|
+
const selectedTitles = new Set(response.selections);
|
|
744
|
+
text += "\n" + theme.fg("dim", "Options:");
|
|
745
|
+
for (const opt of details.options) {
|
|
746
|
+
const marker = selectedTitles.has(opt.title) ? theme.fg("success", "●") : theme.fg("dim", "○");
|
|
747
|
+
text += `\n ${marker} ${theme.fg("dim", opt.title)}${theme.fg("dim", ` — ${opt.description}`)}`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!canAnimate)
|
|
752
|
+
return new Text(scrambleManager.renderStatic(text), 0, 0);
|
|
753
|
+
const scrambled = scrambleManager.updateText(id, "result", stripAnsi(text), now, false).content;
|
|
754
|
+
runScrambleTimer(args, id);
|
|
755
|
+
return new Text(scrambled, 0, 0);
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
// Helper functions
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
function keybindingHint(theme, keybindings, id, label) {
|
|
763
|
+
const keys = keybindings.getKeys(id);
|
|
764
|
+
if (!keys || keys.length === 0)
|
|
765
|
+
return null;
|
|
766
|
+
return `${theme.fg("accent", formatKeyList(keys))} ${theme.fg("dim", label)}`;
|
|
767
|
+
}
|
|
768
|
+
function literalHint(theme, keys, label) {
|
|
769
|
+
return `${theme.fg("accent", keys)} ${theme.fg("dim", label)}`;
|
|
770
|
+
}
|
|
771
|
+
function formatKeyList(keys) {
|
|
772
|
+
if (keys.length === 0)
|
|
773
|
+
return "";
|
|
774
|
+
if (keys.length === 1)
|
|
775
|
+
return keys[0];
|
|
776
|
+
return `${keys.slice(0, -1).join(", ")} or ${keys[keys.length - 1]}`;
|
|
777
|
+
}
|
|
778
|
+
//# sourceMappingURL=ask-user.js.map
|