@mrclrchtr/supi-ask-user 1.3.1 → 1.4.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 +125 -67
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
- package/package.json +2 -2
- package/src/api.ts +19 -0
- package/src/ask-user.ts +65 -131
- package/src/index.ts +23 -1
- package/src/normalize.ts +153 -142
- package/src/render/result.ts +98 -0
- package/src/render/transcript.ts +65 -0
- package/src/render/tree-summary.ts +10 -0
- package/src/schema.ts +41 -38
- package/src/session/controller.ts +163 -0
- package/src/session/lock.ts +19 -0
- package/src/tool/guidance.ts +15 -0
- package/src/types.ts +50 -56
- package/src/ui/choose-renderer.ts +11 -0
- package/src/ui/overlay-actions.ts +42 -0
- package/src/ui/overlay-render.ts +196 -0
- package/src/ui/overlay-view.ts +216 -0
- package/src/ui/overlay.ts +388 -0
- package/src/ui/types.ts +35 -0
- package/src/flow.ts +0 -224
- package/src/format.ts +0 -66
- package/src/render/ui-rich-render-editor.ts +0 -51
- package/src/render/ui-rich-render-env.ts +0 -15
- package/src/render/ui-rich-render-footer.ts +0 -55
- package/src/render/ui-rich-render-markdown.ts +0 -33
- package/src/render/ui-rich-render-notes.ts +0 -80
- package/src/render/ui-rich-render-types.ts +0 -17
- package/src/render/ui-rich-render.ts +0 -323
- package/src/render.ts +0 -95
- package/src/result.ts +0 -90
- package/src/ui/ui-rich-handlers.ts +0 -369
- package/src/ui/ui-rich-inline.ts +0 -77
- package/src/ui/ui-rich-state.ts +0 -179
- package/src/ui/ui-rich.ts +0 -144
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
package/src/ui/ui-rich-state.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
// Shared state types and pure helpers for the rich overlay.
|
|
2
|
-
|
|
3
|
-
import type { Editor } from "@earendil-works/pi-tui";
|
|
4
|
-
import type { QuestionnaireFlow } from "../flow.ts";
|
|
5
|
-
import type { OverlayRenderState, SubMode } from "../render/ui-rich-render-types.ts";
|
|
6
|
-
import type {
|
|
7
|
-
NormalizedQuestion,
|
|
8
|
-
NormalizedStructuredQuestion,
|
|
9
|
-
NormalizedTextQuestion,
|
|
10
|
-
QuestionnaireOutcome,
|
|
11
|
-
} from "../types.ts";
|
|
12
|
-
import { isStructuredQuestion, primaryRecommendationIndex } from "../types.ts";
|
|
13
|
-
|
|
14
|
-
export interface NoteTargetSingle {
|
|
15
|
-
mode: "single";
|
|
16
|
-
questionId: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface NoteTargetMulti {
|
|
20
|
-
mode: "multi";
|
|
21
|
-
questionId: string;
|
|
22
|
-
optionIndex: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type NoteTarget = NoteTargetSingle | NoteTargetMulti;
|
|
26
|
-
|
|
27
|
-
export interface OverlayState extends OverlayRenderState {
|
|
28
|
-
noteTarget?: NoteTarget;
|
|
29
|
-
cachedLines: string[] | undefined;
|
|
30
|
-
cachedWidth: number | undefined;
|
|
31
|
-
maxHeight: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface OverlayDeps {
|
|
35
|
-
flow: QuestionnaireFlow;
|
|
36
|
-
state: OverlayState;
|
|
37
|
-
editor: Editor;
|
|
38
|
-
refresh: () => void;
|
|
39
|
-
finish: (o: QuestionnaireOutcome) => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type InteractiveRow =
|
|
43
|
-
| { kind: "option"; optionIndex: number }
|
|
44
|
-
| { kind: "other" }
|
|
45
|
-
| { kind: "discuss" };
|
|
46
|
-
|
|
47
|
-
export function interactiveRows(question: NormalizedStructuredQuestion): InteractiveRow[] {
|
|
48
|
-
const rows: InteractiveRow[] = question.options.map((_, optionIndex) => ({
|
|
49
|
-
kind: "option",
|
|
50
|
-
optionIndex,
|
|
51
|
-
}));
|
|
52
|
-
if (question.allowOther) rows.push({ kind: "other" });
|
|
53
|
-
if (question.allowDiscuss) rows.push({ kind: "discuss" });
|
|
54
|
-
return rows;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function rowCount(question: NormalizedQuestion): number {
|
|
58
|
-
return isStructuredQuestion(question) ? interactiveRows(question).length : 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function hasPreview(question: NormalizedQuestion): boolean {
|
|
62
|
-
return isStructuredQuestion(question) && question.options.some((option) => !!option.preview);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function initialSubMode(question: NormalizedQuestion | undefined): SubMode {
|
|
66
|
-
if (!question) return "select";
|
|
67
|
-
return question.type === "text" ? "text-input" : "select";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function textDefaultOrAnswer(
|
|
71
|
-
flow: Pick<QuestionnaireFlow, "getAnswer">,
|
|
72
|
-
question: NormalizedTextQuestion,
|
|
73
|
-
): string {
|
|
74
|
-
const answer = flow.getAnswer(question.id);
|
|
75
|
-
if (answer?.source === "text") return answer.value;
|
|
76
|
-
return question.default ?? "";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function resetStateForCurrent(deps: OverlayDeps): void {
|
|
80
|
-
const question = deps.flow.currentQuestion;
|
|
81
|
-
deps.state.subMode = deps.flow.currentMode === "reviewing" ? "select" : initialSubMode(question);
|
|
82
|
-
deps.state.selectedIndex = selectedRowIndex(deps.flow, question);
|
|
83
|
-
deps.state.noteTarget = undefined;
|
|
84
|
-
deps.state.maxHeight = 0;
|
|
85
|
-
const editorText = question?.type === "text" ? textDefaultOrAnswer(deps.flow, question) : "";
|
|
86
|
-
deps.editor.setText(editorText);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function existingStructuredInputValue(
|
|
90
|
-
flow: QuestionnaireFlow,
|
|
91
|
-
questionId: string,
|
|
92
|
-
kind: "other" | "discuss",
|
|
93
|
-
): string {
|
|
94
|
-
const answer = flow.getAnswer(questionId);
|
|
95
|
-
if (kind === "other") return answer?.source === "other" ? answer.value : "";
|
|
96
|
-
return answer?.source === "discuss" ? (answer.value ?? "") : "";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function singleNoteFromAnswer(
|
|
100
|
-
flow: Pick<QuestionnaireFlow, "getAnswer">,
|
|
101
|
-
questionId: string,
|
|
102
|
-
): string | undefined {
|
|
103
|
-
const answer = flow.getAnswer(questionId);
|
|
104
|
-
if (!answer || answer.source !== "choice") return undefined;
|
|
105
|
-
return answer.selections[0]?.note;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function multiNoteMapFromAnswer(
|
|
109
|
-
flow: Pick<QuestionnaireFlow, "getAnswer">,
|
|
110
|
-
questionId: string,
|
|
111
|
-
): Map<number, string> {
|
|
112
|
-
const answer = flow.getAnswer(questionId);
|
|
113
|
-
const map = new Map<number, string>();
|
|
114
|
-
if (!answer || answer.source !== "choice") return map;
|
|
115
|
-
for (const selection of answer.selections) {
|
|
116
|
-
if (selection.note) map.set(selection.optionIndex, selection.note);
|
|
117
|
-
}
|
|
118
|
-
return map;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function mergedMultiNoteMap(deps: OverlayDeps, questionId: string): Map<number, string> {
|
|
122
|
-
const answerMap = multiNoteMapFromAnswer(deps.flow, questionId);
|
|
123
|
-
const staged = deps.state.stagedMultiNotes.get(questionId);
|
|
124
|
-
if (!staged) return answerMap;
|
|
125
|
-
const merged = new Map(answerMap);
|
|
126
|
-
for (const [optionIndex, note] of staged.entries()) merged.set(optionIndex, note);
|
|
127
|
-
return merged;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function isEditorMode(mode: SubMode): boolean {
|
|
131
|
-
return (
|
|
132
|
-
mode === "text-input" ||
|
|
133
|
-
mode === "other-input" ||
|
|
134
|
-
mode === "discuss-input" ||
|
|
135
|
-
mode === "note-input"
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function selectedIndexesForQuestion(
|
|
140
|
-
flow: Pick<QuestionnaireFlow, "getAnswer">,
|
|
141
|
-
state: Pick<OverlayRenderState, "stagedSelections">,
|
|
142
|
-
question: NormalizedStructuredQuestion,
|
|
143
|
-
): number[] {
|
|
144
|
-
const answer = flow.getAnswer(question.id);
|
|
145
|
-
if (answer?.source === "other" || answer?.source === "discuss") return [];
|
|
146
|
-
const staged = state.stagedSelections.get(question.id);
|
|
147
|
-
if (staged) return [...staged];
|
|
148
|
-
if (!answer) {
|
|
149
|
-
if (question.defaultIndexes.length > 0) return [...question.defaultIndexes];
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
if (answer.source === "choice") return answer.selections.map((s) => s.optionIndex);
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function selectedRowIndex(
|
|
157
|
-
flow: Pick<QuestionnaireFlow, "getAnswer">,
|
|
158
|
-
question: NormalizedQuestion | undefined,
|
|
159
|
-
): number {
|
|
160
|
-
if (!question || question.type === "text") return 0;
|
|
161
|
-
const rows = interactiveRows(question);
|
|
162
|
-
const answer = flow.getAnswer(question.id);
|
|
163
|
-
if (!answer) {
|
|
164
|
-
const defaultIdx = isStructuredQuestion(question) ? question.defaultIndexes[0] : undefined;
|
|
165
|
-
if (defaultIdx !== undefined) return defaultIdx;
|
|
166
|
-
const recommended = primaryRecommendationIndex(question);
|
|
167
|
-
return recommended ?? 0;
|
|
168
|
-
}
|
|
169
|
-
switch (answer.source) {
|
|
170
|
-
case "choice":
|
|
171
|
-
return answer.selections[0]?.optionIndex ?? 0;
|
|
172
|
-
case "other":
|
|
173
|
-
return rows.findIndex((row) => row.kind === "other");
|
|
174
|
-
case "discuss":
|
|
175
|
-
return rows.findIndex((row) => row.kind === "discuss");
|
|
176
|
-
case "text":
|
|
177
|
-
return 0;
|
|
178
|
-
}
|
|
179
|
-
}
|
package/src/ui/ui-rich.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// Rich questionnaire UI built on `ctx.ui.custom()`. Supports explicit choice
|
|
2
|
-
// (single and multi-select), notes, other, discuss, preview, and review flows. Returns a
|
|
3
|
-
// QuestionnaireOutcome whose terminal state is owned by the shared flow.
|
|
4
|
-
|
|
5
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { type Component, Editor, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
7
|
-
import { QuestionnaireFlow } from "../flow.ts";
|
|
8
|
-
import { renderOverlay } from "../render/ui-rich-render.ts";
|
|
9
|
-
import type { NormalizedQuestionnaire, QuestionnaireOutcome } from "../types.ts";
|
|
10
|
-
import { handleOverlayInput, onEditorSubmit } from "./ui-rich-handlers.ts";
|
|
11
|
-
import {
|
|
12
|
-
initialSubMode,
|
|
13
|
-
type OverlayState,
|
|
14
|
-
selectedRowIndex,
|
|
15
|
-
textDefaultOrAnswer,
|
|
16
|
-
} from "./ui-rich-state.ts";
|
|
17
|
-
|
|
18
|
-
export interface RichCustomOptions {
|
|
19
|
-
overlay?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RichUiHost {
|
|
23
|
-
custom<T>(
|
|
24
|
-
factory: (
|
|
25
|
-
tui: TUI,
|
|
26
|
-
theme: Theme,
|
|
27
|
-
// biome-ignore lint/suspicious/noExplicitAny: keybindings parameter type isn't needed here
|
|
28
|
-
keybindings: any,
|
|
29
|
-
done: (result: T) => void,
|
|
30
|
-
) => Component & { dispose?(): void },
|
|
31
|
-
options?: RichCustomOptions,
|
|
32
|
-
): Promise<T> | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface RunRichOptions {
|
|
36
|
-
ui: RichUiHost;
|
|
37
|
-
signal?: AbortSignal;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function runRichQuestionnaire(
|
|
41
|
-
questionnaire: NormalizedQuestionnaire,
|
|
42
|
-
opts: RunRichOptions,
|
|
43
|
-
): Promise<QuestionnaireOutcome | "unsupported"> {
|
|
44
|
-
const flow = new QuestionnaireFlow(questionnaire.questions, questionnaire.allowSkip);
|
|
45
|
-
// Short-circuit before opening the overlay if we were already aborted.
|
|
46
|
-
// Otherwise signal.addEventListener("abort", …) would never fire and the
|
|
47
|
-
// overlay would stay open until the user dismissed it manually.
|
|
48
|
-
if (opts.signal?.aborted) {
|
|
49
|
-
flow.abort();
|
|
50
|
-
return flow.outcome();
|
|
51
|
-
}
|
|
52
|
-
// pi-tui's declarative keybinding system is not used here — the overlay
|
|
53
|
-
// implements its own dynamic input handling because footer hints change
|
|
54
|
-
// per mode and question type (e.g. Space toggle in multi-select vs Enter
|
|
55
|
-
// confirm in choice).
|
|
56
|
-
const promise = opts.ui.custom<QuestionnaireOutcome>((tui, theme, _kb, done) =>
|
|
57
|
-
buildOverlay({ tui, theme, flow, signal: opts.signal, done }),
|
|
58
|
-
);
|
|
59
|
-
if (!promise) return "unsupported";
|
|
60
|
-
return promise;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface BuildOverlayArgs {
|
|
64
|
-
tui: TUI;
|
|
65
|
-
theme: Theme;
|
|
66
|
-
flow: QuestionnaireFlow;
|
|
67
|
-
signal: AbortSignal | undefined;
|
|
68
|
-
done: (result: QuestionnaireOutcome) => void;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function buildOverlay(args: BuildOverlayArgs): Component {
|
|
72
|
-
const { tui, theme, flow, signal, done } = args;
|
|
73
|
-
const state: OverlayState = {
|
|
74
|
-
selectedIndex: selectedRowIndex(flow, flow.currentQuestion),
|
|
75
|
-
subMode: initialSubMode(flow.currentQuestion),
|
|
76
|
-
stagedSelections: new Map(),
|
|
77
|
-
stagedSingleNotes: new Map(),
|
|
78
|
-
stagedMultiNotes: new Map(),
|
|
79
|
-
noteTarget: undefined,
|
|
80
|
-
cachedLines: undefined,
|
|
81
|
-
cachedWidth: undefined,
|
|
82
|
-
maxHeight: 0,
|
|
83
|
-
};
|
|
84
|
-
const editor = new Editor(tui, makeEditorTheme(theme));
|
|
85
|
-
const initialQuestion = flow.currentQuestion;
|
|
86
|
-
if (initialQuestion?.type === "text") {
|
|
87
|
-
editor.setText(textDefaultOrAnswer(flow, initialQuestion));
|
|
88
|
-
}
|
|
89
|
-
const refresh = () => {
|
|
90
|
-
state.cachedLines = undefined;
|
|
91
|
-
tui.requestRender();
|
|
92
|
-
};
|
|
93
|
-
const finish = (outcome: QuestionnaireOutcome) => done(outcome);
|
|
94
|
-
|
|
95
|
-
signal?.addEventListener("abort", () => {
|
|
96
|
-
flow.abort();
|
|
97
|
-
done(flow.outcome());
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const deps = { flow, state, editor, refresh, finish };
|
|
101
|
-
editor.onSubmit = (value) => onEditorSubmit(value, deps);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
render: (width) => {
|
|
105
|
-
// pi's TUI does not call invalidate() on terminal resize, so a width
|
|
106
|
-
// change has to invalidate the cache here or we'd return stale lines
|
|
107
|
-
// truncated for the previous width.
|
|
108
|
-
if (state.cachedWidth !== width) {
|
|
109
|
-
state.cachedLines = undefined;
|
|
110
|
-
state.cachedWidth = width;
|
|
111
|
-
state.maxHeight = 0;
|
|
112
|
-
}
|
|
113
|
-
if (!state.cachedLines) {
|
|
114
|
-
state.cachedLines = renderOverlay({ width, theme, flow, state, editor });
|
|
115
|
-
// Stabilize height — prevent shrinkage that triggers pi-tui's
|
|
116
|
-
// viewport tracking bug with differential rendering.
|
|
117
|
-
if (state.cachedLines.length > state.maxHeight) {
|
|
118
|
-
state.maxHeight = state.cachedLines.length;
|
|
119
|
-
}
|
|
120
|
-
while (state.cachedLines.length < state.maxHeight) {
|
|
121
|
-
state.cachedLines.push("");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return state.cachedLines;
|
|
125
|
-
},
|
|
126
|
-
invalidate: () => {
|
|
127
|
-
state.cachedLines = undefined;
|
|
128
|
-
},
|
|
129
|
-
handleInput: (data: string) => handleOverlayInput(data, deps),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function makeEditorTheme(theme: Theme): EditorTheme {
|
|
134
|
-
return {
|
|
135
|
-
borderColor: (text) => theme.fg("accent", text),
|
|
136
|
-
selectList: {
|
|
137
|
-
selectedPrefix: (text) => theme.fg("accent", text),
|
|
138
|
-
selectedText: (text) => theme.fg("accent", text),
|
|
139
|
-
description: (text) => theme.fg("muted", text),
|
|
140
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
141
|
-
noMatch: (text) => theme.fg("warning", text),
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
File without changes
|
/package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts}
RENAMED
|
File without changes
|
|
File without changes
|