pi-cc-extensions 0.8.8 → 0.8.9
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 +21 -4
- package/extensions/ask-user-question/LICENSE +21 -0
- package/extensions/ask-user-question/README.md +63 -0
- package/extensions/ask-user-question/ask-user-question.ts +244 -0
- package/extensions/ask-user-question/config.ts +75 -0
- package/extensions/ask-user-question/events.ts +55 -0
- package/extensions/ask-user-question/index.ts +17 -0
- package/extensions/ask-user-question/reconcile.ts +47 -0
- package/extensions/ask-user-question/rpc-fallback.ts +160 -0
- package/extensions/ask-user-question/state/build-questionnaire.ts +291 -0
- package/extensions/ask-user-question/state/dialog-height.ts +16 -0
- package/extensions/ask-user-question/state/key-router.ts +269 -0
- package/extensions/ask-user-question/state/questionnaire-session.ts +196 -0
- package/extensions/ask-user-question/state/row-intent.ts +143 -0
- package/extensions/ask-user-question/state/selectors/contract.ts +24 -0
- package/extensions/ask-user-question/state/selectors/derivations.ts +40 -0
- package/extensions/ask-user-question/state/selectors/focus.ts +17 -0
- package/extensions/ask-user-question/state/selectors/projections.ts +99 -0
- package/extensions/ask-user-question/state/state-reducer.ts +282 -0
- package/extensions/ask-user-question/state/state.ts +49 -0
- package/extensions/ask-user-question/tool/format-answer.ts +29 -0
- package/extensions/ask-user-question/tool/response-envelope.ts +47 -0
- package/extensions/ask-user-question/tool/types.ts +145 -0
- package/extensions/ask-user-question/tool/validate-questionnaire.ts +56 -0
- package/extensions/ask-user-question/view/component-binding.ts +45 -0
- package/extensions/ask-user-question/view/components/inline-input.ts +96 -0
- package/extensions/ask-user-question/view/components/multi-select-view.ts +191 -0
- package/extensions/ask-user-question/view/components/option-list-view.ts +68 -0
- package/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +76 -0
- package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +108 -0
- package/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +86 -0
- package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +200 -0
- package/extensions/ask-user-question/view/components/preview/preview-pane.ts +226 -0
- package/extensions/ask-user-question/view/components/submit-picker.ts +64 -0
- package/extensions/ask-user-question/view/components/tab-bar.ts +57 -0
- package/extensions/ask-user-question/view/components/wrapping-select.ts +291 -0
- package/extensions/ask-user-question/view/dialog-builder.ts +205 -0
- package/extensions/ask-user-question/view/props-adapter.ts +123 -0
- package/extensions/ask-user-question/view/stateful-view.ts +24 -0
- package/extensions/ask-user-question/view/tab-components.ts +16 -0
- package/extensions/ask-user-question/view/tab-content-strategy.ts +249 -0
- package/package.json +10 -4
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, Container, type Input, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatAnswerScalar } from "../tool/format-answer.js";
|
|
4
|
+
import type { QuestionData } from "../tool/types.js";
|
|
5
|
+
import type { PreviewPane, PreviewPaneProps } from "./components/preview/preview-pane.js";
|
|
6
|
+
import {
|
|
7
|
+
type DialogState,
|
|
8
|
+
HINT_PART_CANCEL,
|
|
9
|
+
HINT_PART_COLLAPSE,
|
|
10
|
+
HINT_PART_ENTER,
|
|
11
|
+
HINT_PART_NAV,
|
|
12
|
+
HINT_PART_NOTES,
|
|
13
|
+
HINT_PART_TAB,
|
|
14
|
+
HINT_PART_TOGGLE,
|
|
15
|
+
INCOMPLETE_WARNING_PREFIX,
|
|
16
|
+
READY_PROMPT,
|
|
17
|
+
REVIEW_HEADING,
|
|
18
|
+
} from "./dialog-builder.js";
|
|
19
|
+
import type { StatefulView } from "./stateful-view.js";
|
|
20
|
+
import type { TabComponents } from "./tab-components.js";
|
|
21
|
+
|
|
22
|
+
const NOTES_HEADER = "Notes:";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Single-row, width-clipped chrome cell. The footer row count is invariant
|
|
26
|
+
* (`QuestionTabStrategy.footerRowCount = 2`) — pi-tui's `Text` word-wraps when
|
|
27
|
+
* the styled hint exceeds `width`, inflating that row count and desyncing the
|
|
28
|
+
* `bodyHeight + footerRowCount` math in `DialogView.render`. Clipping with
|
|
29
|
+
* `truncateToWidth` (ANSI-aware, matches `multi-select-view.ts` usage) keeps
|
|
30
|
+
* the hint on one line; the collapse affordance falls off the right edge with
|
|
31
|
+
* `…` on terminals too narrow to advertise it.
|
|
32
|
+
*/
|
|
33
|
+
class OneLineClippedText implements Component {
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly text: string,
|
|
36
|
+
private readonly paddingLeft: number = 0,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
render(width: number): string[] {
|
|
40
|
+
const pad = " ".repeat(this.paddingLeft);
|
|
41
|
+
const avail = Math.max(0, width - this.paddingLeft);
|
|
42
|
+
return [pad + truncateToWidth(this.text, avail, "…", false)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
invalidate(): void {}
|
|
46
|
+
|
|
47
|
+
handleInput(_data: string): void {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-tab content provider. Pure functional — closes over construction-time
|
|
52
|
+
* config; per-tick state threads through method args. The chrome wrapper
|
|
53
|
+
* enforces height equality across tabs via `bodyHeight + footerRowCount`.
|
|
54
|
+
*/
|
|
55
|
+
export interface TabContentStrategy {
|
|
56
|
+
/** Total RENDERED footer rows — MUST equal what `footerRows()` actually emits. Drives residual math. */
|
|
57
|
+
readonly footerRowCount: number;
|
|
58
|
+
|
|
59
|
+
/** Variable rows above the body, after top chrome (border + tabBar + Spacer). */
|
|
60
|
+
headingRows(state: DialogState): Component[];
|
|
61
|
+
|
|
62
|
+
/** Body Component placed at the body slot. */
|
|
63
|
+
bodyComponent(state: DialogState): Component;
|
|
64
|
+
|
|
65
|
+
/** Natural rendered height of `bodyComponent(state)` at given width. */
|
|
66
|
+
bodyHeight(width: number, state: DialogState): number;
|
|
67
|
+
|
|
68
|
+
/** Optional rows between body's trailing Spacer and the bottom border. */
|
|
69
|
+
midRows(state: DialogState): Component[];
|
|
70
|
+
|
|
71
|
+
/** Footer rows below the bottom border. Rendered row count MUST equal `footerRowCount`. */
|
|
72
|
+
footerRows(state: DialogState): Component[];
|
|
73
|
+
|
|
74
|
+
/** Row range of the focused item within the body's rendered output, or undefined if no interactive focus. */
|
|
75
|
+
focusedItemRowRange(width: number, state: DialogState): [number, number] | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface QuestionTabStrategyConfig {
|
|
79
|
+
theme: Theme;
|
|
80
|
+
questions: readonly QuestionData[];
|
|
81
|
+
getPreviewPane: () => StatefulView<PreviewPaneProps>;
|
|
82
|
+
tabsByIndex: ReadonlyArray<TabComponents>;
|
|
83
|
+
notesInput: Input;
|
|
84
|
+
isMulti: boolean;
|
|
85
|
+
getCurrentBodyHeight: (width: number) => number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class QuestionTabStrategy implements TabContentStrategy {
|
|
89
|
+
/** Spacer(1) + OneLineClippedText(hint, 1) = 2 rendered rows. */
|
|
90
|
+
readonly footerRowCount = 2;
|
|
91
|
+
|
|
92
|
+
constructor(private readonly config: QuestionTabStrategyConfig) {}
|
|
93
|
+
|
|
94
|
+
headingRows(state: DialogState): Component[] {
|
|
95
|
+
const out: Component[] = [];
|
|
96
|
+
const question = this.config.questions[state.currentTab];
|
|
97
|
+
// In multi-question mode the tab bar already shows the header; suppress the inline badge.
|
|
98
|
+
if (!this.config.isMulti && question?.header && question.header.length > 0) {
|
|
99
|
+
out.push(new Text(this.config.theme.bg("selectedBg", ` ${question.header} `), 1, 0));
|
|
100
|
+
out.push(new Spacer(1));
|
|
101
|
+
}
|
|
102
|
+
if (question) {
|
|
103
|
+
out.push(new Text(this.config.theme.bold(question.question), 1, 0));
|
|
104
|
+
out.push(new Spacer(1));
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bodyComponent(state: DialogState): Component {
|
|
110
|
+
const question = this.config.questions[state.currentTab];
|
|
111
|
+
const mso = this.config.tabsByIndex[state.currentTab]?.multiSelect;
|
|
112
|
+
if (question?.multiSelect === true && mso) return mso;
|
|
113
|
+
return this.config.getPreviewPane();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
bodyHeight(width: number, _state: DialogState): number {
|
|
117
|
+
return this.config.getCurrentBodyHeight(width);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
midRows(state: DialogState): Component[] {
|
|
121
|
+
if (!state.notesVisible) return [];
|
|
122
|
+
return [
|
|
123
|
+
new Text(this.config.theme.fg("muted", NOTES_HEADER), 1, 0),
|
|
124
|
+
this.config.notesInput,
|
|
125
|
+
new Spacer(1),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
footerRows(state: DialogState): Component[] {
|
|
130
|
+
const question = this.config.questions[state.currentTab];
|
|
131
|
+
// OneLineClippedText (not pi-tui `Text`) — `buildHintText` includes the collapse
|
|
132
|
+
// affordance, pushing the rendered string past 80 columns; `Text` would wrap and
|
|
133
|
+
// break the strategy's `footerRowCount = 2` invariant. Clipping on narrow terminals
|
|
134
|
+
// drops the trailing parts (collapse hint first, then cancel) with `…`.
|
|
135
|
+
return [
|
|
136
|
+
new Spacer(1),
|
|
137
|
+
new OneLineClippedText(this.config.theme.fg("dim", buildHintText(question, this.config.isMulti, state)), 1),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
focusedItemRowRange(width: number, state: DialogState): [number, number] | undefined {
|
|
142
|
+
const question = this.config.questions[state.currentTab];
|
|
143
|
+
const mso = this.config.tabsByIndex[state.currentTab]?.multiSelect;
|
|
144
|
+
if (question?.multiSelect === true && mso) return mso.focusedItemRowRange(width);
|
|
145
|
+
return (this.config.getPreviewPane() as unknown as PreviewPane).focusedItemRowRange(width);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface SubmitTabStrategyConfig {
|
|
150
|
+
theme: Theme;
|
|
151
|
+
questions: readonly QuestionData[];
|
|
152
|
+
submitPicker: Component | undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class SubmitTabStrategy implements TabContentStrategy {
|
|
156
|
+
/** Spacer(1) + Text(prompt, 1) + Spacer(1) + submitPicker(2) = 5 rendered rows. Fallback path lands at 5 via 2 trailing Spacer(1)s. */
|
|
157
|
+
readonly footerRowCount = 5;
|
|
158
|
+
|
|
159
|
+
constructor(private readonly config: SubmitTabStrategyConfig) {}
|
|
160
|
+
|
|
161
|
+
headingRows(_state: DialogState): Component[] {
|
|
162
|
+
return [
|
|
163
|
+
new Text(this.config.theme.bold(this.config.theme.fg("accent", REVIEW_HEADING)), 1, 0),
|
|
164
|
+
new Spacer(1),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
bodyComponent(state: DialogState): Component {
|
|
169
|
+
const c = new Container();
|
|
170
|
+
for (let i = 0; i < this.config.questions.length; i++) {
|
|
171
|
+
const q = this.config.questions[i];
|
|
172
|
+
const a = state.answers.get(i);
|
|
173
|
+
if (!a) continue;
|
|
174
|
+
const label = q.header && q.header.length > 0 ? q.header : `Q${i + 1}`;
|
|
175
|
+
const answerText = formatAnswerScalar(a, "summary");
|
|
176
|
+
c.addChild(new Text(this.config.theme.fg("muted", ` ● ${label}`), 1, 0));
|
|
177
|
+
c.addChild(
|
|
178
|
+
new Text(` ${this.config.theme.fg("muted", "→")} ${this.config.theme.fg("text", answerText)}`, 1, 0),
|
|
179
|
+
);
|
|
180
|
+
if (a.notes && a.notes.length > 0) {
|
|
181
|
+
c.addChild(new Text(this.config.theme.fg("dim", ` notes: ${a.notes}`), 1, 0));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return c;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
bodyHeight(width: number, state: DialogState): number {
|
|
188
|
+
return this.bodyComponent(state).render(width).length;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
midRows(_state: DialogState): Component[] {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
footerRows(state: DialogState): Component[] {
|
|
196
|
+
const missing: string[] = [];
|
|
197
|
+
for (let i = 0; i < this.config.questions.length; i++) {
|
|
198
|
+
const q = this.config.questions[i];
|
|
199
|
+
if (!state.answers.has(i)) {
|
|
200
|
+
missing.push(q.header && q.header.length > 0 ? q.header : `Q${i + 1}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const promptText =
|
|
204
|
+
missing.length === 0
|
|
205
|
+
? this.config.theme.fg("muted", READY_PROMPT)
|
|
206
|
+
: this.config.theme.fg(
|
|
207
|
+
"warning",
|
|
208
|
+
`${INCOMPLETE_WARNING_PREFIX} ${missing.join(", ")}`,
|
|
209
|
+
);
|
|
210
|
+
const out: Component[] = [new Spacer(1), new Text(promptText, 1, 0), new Spacer(1)];
|
|
211
|
+
if (this.config.submitPicker) {
|
|
212
|
+
out.push(this.config.submitPicker);
|
|
213
|
+
} else {
|
|
214
|
+
// Padding when the picker isn't wired — keeps rendered row count at footerRowCount=5.
|
|
215
|
+
out.push(new Spacer(1));
|
|
216
|
+
out.push(new Spacer(1));
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
focusedItemRowRange(_width: number, _state: DialogState): [number, number] | undefined {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Build the controls hint line. Order:
|
|
228
|
+
* Enter · ↑/↓ [· Space toggle] [· n notes] [· Tab switch] · Esc · Ctrl+] collapse
|
|
229
|
+
*
|
|
230
|
+
* `NOTES` is now part of the resting (notes-closed) core — it renders on every
|
|
231
|
+
* question tab and drops when `state.notesVisible` flips the editor open OR while
|
|
232
|
+
* `state.inputMode` has the "Type something." row capturing text (`n` would insert
|
|
233
|
+
* a literal character there, so advertising it would lie). The collapse affordance
|
|
234
|
+
* is appended last so the resting core stays a contiguous substring even when the
|
|
235
|
+
* trailing part is clipped by `OneLineClippedText` on terminals < ~95 cols. This
|
|
236
|
+
* is the trade we picked over wrapping (which would inflate `footerRowCount` and
|
|
237
|
+
* desync the height math in `DialogView.render`).
|
|
238
|
+
*/
|
|
239
|
+
export function buildHintText(question: QuestionData | undefined, isMulti: boolean, state: DialogState): string {
|
|
240
|
+
const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
|
|
241
|
+
if (question?.multiSelect === true) parts.push(HINT_PART_TOGGLE);
|
|
242
|
+
if (question && !state.notesVisible && !state.inputMode) {
|
|
243
|
+
parts.push(HINT_PART_NOTES);
|
|
244
|
+
}
|
|
245
|
+
if (isMulti) parts.push(HINT_PART_TAB);
|
|
246
|
+
parts.push(HINT_PART_CANCEL);
|
|
247
|
+
parts.push(HINT_PART_COLLAPSE);
|
|
248
|
+
return parts.join(" · ");
|
|
249
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
|
|
3
|
+
"version": "0.8.9",
|
|
4
|
+
"description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
9
|
"pi-coding-agent",
|
|
10
|
-
"claude-code"
|
|
10
|
+
"claude-code",
|
|
11
|
+
"questionnaire"
|
|
11
12
|
],
|
|
12
13
|
"homepage": "https://github.com/minuque/pi-cc-extensions#readme",
|
|
13
14
|
"repository": {
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
"./extensions/claude-code-style.ts",
|
|
29
30
|
"./extensions/context.ts",
|
|
30
31
|
"./extensions/session-reference.ts",
|
|
31
|
-
"./extensions/agent-autocomplete.ts"
|
|
32
|
+
"./extensions/agent-autocomplete.ts",
|
|
33
|
+
"./extensions/ask-user-question/index.ts"
|
|
32
34
|
],
|
|
33
35
|
"themes": [
|
|
34
36
|
"./themes/github-dark-default.json"
|
|
@@ -43,6 +45,10 @@
|
|
|
43
45
|
"release:minor": "npm version minor && npm publish",
|
|
44
46
|
"release:major": "npm version major && npm publish"
|
|
45
47
|
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@juicesharp/rpiv-config": "^2.1.0",
|
|
50
|
+
"typebox": "^1.1.24"
|
|
51
|
+
},
|
|
46
52
|
"peerDependencies": {
|
|
47
53
|
"@earendil-works/pi-ai": "*",
|
|
48
54
|
"@earendil-works/pi-coding-agent": "*",
|