pi-pier 0.4.1 → 0.4.2
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 +1 -2
- package/package.json +1 -1
- package/schemas/efficiency-config.schema.json +6 -0
- package/src/ask-dialog.ts +9 -2
- package/src/ask-user.ts +142 -74
- package/src/compact-coordinator.ts +204 -33
- package/src/compact-economics-core.ts +62 -29
- package/src/config-catalog-core.ts +1 -0
- package/src/delegation-core.ts +1 -1
- package/src/efficiency-config-core.ts +16 -2
- package/src/efficiency-store.ts +42 -5
- package/src/herdr-client.ts +15 -5
- package/src/index-pipe.ts +0 -8
- package/src/index.ts +69 -85
- package/src/jev-client.ts +32 -7
- package/src/jev-core.ts +70 -33
- package/src/observation-core.ts +13 -1
- package/src/pipe-channel.ts +0 -8
- package/src/plugins/heat-plan.ts +18 -3
- package/src/plugins/observation.ts +118 -46
- package/src/plugins/subagent.ts +53 -40
- package/src/plugins/terminal.ts +192 -47
- package/src/plugins/todo.ts +43 -21
- package/src/reducer-invoker.ts +27 -13
- package/src/renderers.ts +1 -13
- package/src/roles/worker-default.json +3 -3
- package/src/settle-wake-core.ts +8 -2
- package/src/subagent-core.ts +41 -3
- package/src/subagent-poller.ts +52 -18
- package/src/subagent-session.ts +67 -4
- package/src/subagent-spawn.ts +6 -5
- package/src/terminal-core.ts +111 -17
- package/src/todo-core.ts +47 -2
- package/src/todo-read-hook.ts +31 -17
- package/src/todo-reminder-core.ts +9 -5
- package/src/todo-window.ts +8 -4
- package/src/todos-service.ts +12 -0
- package/src/vocab.ts +7 -1
package/README.md
CHANGED
|
@@ -43,8 +43,7 @@ The installer also verifies node / pi / herdr versions.
|
|
|
43
43
|
| Tool | Purpose |
|
|
44
44
|
|---|---|
|
|
45
45
|
| `todo_write` | Full-replacement todo list. Session JSONL is the single source of truth; correct rollback on branch switches. Projected live onto the pane title (`▶i ○p ■b ✓c (N/M) · current task`). `/todos` command to view/edit/unblock |
|
|
46
|
-
| `subagent` | Delegate self-contained subtasks to an isolated pi session in its own herdr pane. Actions: `spawn` (default; foreground / parallel / background), `list`, `send`, `interrupt`, `resume`, `output` (incremental preview of a running subagent's pane output), `role` (switch that worker's role profile mid-session — pi records the toolset change as a transcript delta, so it applies on the worker's next request and survives resume). Every spawned task gets a stable slug: outputs print `slug@paneId`, and `agentId` accepts the slug, the pane id, or the combined form |
|
|
47
|
-
| `report_progress` | Worker panes only: send the delegating master a short checkpoint update without ending the task. Arrives in the master's context as a display-less custom message (no turn, no chat flood), rendered as a `⤴ progress` card |
|
|
46
|
+
| `subagent` | Delegate self-contained subtasks to an isolated pi session in its own herdr pane. Actions: `spawn` (default; foreground / parallel / background), `list`, `send`, `interrupt`, `resume`, `output` (incremental preview of a running subagent's pane output), `role` (switch that worker's role profile mid-session — pi records the toolset change as a transcript delta, so it applies on the worker's next request and survives resume), `help` (the full manual — placement, isolate, roles, ledger — kept out of the per-request description). Every spawned task gets a stable slug: outputs print `slug@paneId`, and `agentId` accepts the slug, the pane id, or the combined form |
|
|
48
47
|
| `terminal` | Persistent interactive shells in dedicated herdr panes. Actions: `open`, `send`, `read`, `signal`, `close`, `list` |
|
|
49
48
|
|
|
50
49
|
### Commands
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pier",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "pier: pi extension that fuses pi agent sessions with the herdr pane/tab orchestrator — todo_write loop closure + interactive subagents in isolated panes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
},
|
|
21
21
|
"firstCompactionRequestScale": { "type": "number", "default": 2.0, "minimum": 1.0 },
|
|
22
22
|
"subsequentCompactionMargin": { "type": "number", "default": 1.5, "minimum": 1.0 },
|
|
23
|
+
"summaryThinkingLevel": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
26
|
+
"default": "low",
|
|
27
|
+
"description": "压缩摘要请求的 thinking 等级上限(只降不升,结束后恢复会话等级)。inherit 表示沿用会话等级"
|
|
28
|
+
},
|
|
23
29
|
"keepRecentTokens": {
|
|
24
30
|
"type": "integer",
|
|
25
31
|
"default": 20000,
|
package/src/ask-dialog.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface DialogConfig {
|
|
|
25
25
|
recommended?: number;
|
|
26
26
|
/** true = toggle list (space toggles, enter confirms the set); false = pick-one (enter/space takes the cursor row). */
|
|
27
27
|
multi: boolean;
|
|
28
|
+
/** Multi mode only: toggles to restore when the dialog reopens (an empty Other answer does not lose them). */
|
|
29
|
+
initialSelected?: readonly boolean[];
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface DialogState {
|
|
@@ -38,7 +40,8 @@ type DialogKey = 'up' | 'down' | 'toggle' | 'confirm' | 'cancel' | 'all' | 'igno
|
|
|
38
40
|
|
|
39
41
|
export type DialogOutcome =
|
|
40
42
|
| { kind: 'selected'; labels: string[] }
|
|
41
|
-
|
|
43
|
+
/** `labels` are the toggled rows at the moment Other was taken, so a custom answer keeps them. */
|
|
44
|
+
| { kind: 'other'; labels: string[] }
|
|
42
45
|
| { kind: 'cancel' };
|
|
43
46
|
|
|
44
47
|
export interface DialogStep {
|
|
@@ -94,7 +97,7 @@ export function stepDialog(state: DialogState, key: DialogKey, config: DialogCon
|
|
|
94
97
|
}
|
|
95
98
|
case 'toggle':
|
|
96
99
|
case 'confirm': {
|
|
97
|
-
if (isOtherRow(state, config)) return { state, outcome: { kind: 'other' } };
|
|
100
|
+
if (isOtherRow(state, config)) return { state, outcome: { kind: 'other', labels: selectedLabels(state, config) } };
|
|
98
101
|
if (!config.multi) {
|
|
99
102
|
// Pick-one: enter and space both take the cursor row.
|
|
100
103
|
return { state, outcome: { kind: 'selected', labels: [config.options[state.cursor]!.label] } };
|
|
@@ -234,7 +237,11 @@ export function dialogFooter(
|
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
export function createDialogComponent(opts: DialogUiOptions): DialogComponent {
|
|
240
|
+
const initial = opts.config.initialSelected;
|
|
237
241
|
let state = createDialogState(opts.config);
|
|
242
|
+
if (initial && initial.length === opts.config.options.length) {
|
|
243
|
+
state = { ...state, selected: opts.config.options.map((_, i) => initial[i] === true) };
|
|
244
|
+
}
|
|
238
245
|
let finished = false;
|
|
239
246
|
const onAbort = (): void => finish({ kind: 'cancel' });
|
|
240
247
|
function finish(outcome: DialogOutcome): void {
|
package/src/ask-user.ts
CHANGED
|
@@ -37,49 +37,38 @@ const RESERVED_LOWER: Record<string, true> = {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
const OptionSchema = Type.Object({
|
|
40
|
-
label: Type.String({ description: 'Short
|
|
41
|
-
description: Type.Optional(Type.String({ description: 'Tradeoff
|
|
40
|
+
label: Type.String({ description: 'Short label (1-5 words)' }),
|
|
41
|
+
description: Type.Optional(Type.String({ description: 'Tradeoff of this choice' })),
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
export const ASK_PARAMETERS = Type.Object({
|
|
45
|
-
question: Type.Optional(Type.String({ description: '
|
|
46
|
-
options: Type.Optional(Type.Array(OptionSchema, {
|
|
47
|
-
|
|
48
|
-
})),
|
|
49
|
-
|
|
50
|
-
allowOther: Type.Optional(Type.Boolean({ description: 'Set false to offer the authored choices only, with no free-text row (default true).' })),
|
|
51
|
-
recommended: Type.Optional(Type.Number({ description: '0-based index of the recommended option; UI adds (Recommended)' })),
|
|
45
|
+
question: Type.Optional(Type.String({ description: 'Flat form: the question. Required unless questions is set.' })),
|
|
46
|
+
options: Type.Optional(Type.Array(OptionSchema, { description: 'Flat form: 2-5 choices' })),
|
|
47
|
+
multi: Type.Optional(Type.Boolean({ description: 'Allow several selections (default false)' })),
|
|
48
|
+
allowOther: Type.Optional(Type.Boolean({ description: 'false = authored choices only, no free-text row (default true)' })),
|
|
49
|
+
recommended: Type.Optional(Type.Number({ description: '0-based default option; the UI adds "(Recommended)", never write it in the label' })),
|
|
52
50
|
questions: Type.Optional(Type.Array(
|
|
53
51
|
Type.Object({
|
|
54
52
|
id: Type.Optional(Type.String({ description: 'Stable id for mapping answers' })),
|
|
55
|
-
question: Type.String(
|
|
56
|
-
options: Type.Array(OptionSchema, {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
allowOther: Type.Optional(Type.Boolean({ description: 'Set false to offer the authored choices only, with no free-text row (default true).' })),
|
|
61
|
-
recommended: Type.Optional(Type.Number({ description: '0-based recommended option index' })),
|
|
53
|
+
question: Type.String(),
|
|
54
|
+
options: Type.Array(OptionSchema, { description: '2-5 choices' }),
|
|
55
|
+
multi: Type.Optional(Type.Boolean()),
|
|
56
|
+
allowOther: Type.Optional(Type.Boolean()),
|
|
57
|
+
recommended: Type.Optional(Type.Number()),
|
|
62
58
|
}),
|
|
63
|
-
{ description: '1-4
|
|
59
|
+
{ description: 'Batched form: 1-4 questions; wins over top-level question/options' },
|
|
64
60
|
)),
|
|
65
61
|
});
|
|
66
62
|
|
|
67
63
|
export const ASK_TOOL_DESCRIPTION = [
|
|
68
|
-
'Ask the human a
|
|
69
|
-
'
|
|
70
|
-
'While waiting, the pane shows as blocked in herdr (the human sees it and can step in).',
|
|
71
|
-
'Prefer 2-5 options with short labels; put tradeoffs in description.',
|
|
72
|
-
'Do NOT include an "Other" option — the UI appends "Other (type your own)" automatically.',
|
|
73
|
-
'Use recommended (0-based) to mark the default; "(Recommended)" is added automatically — do NOT write it in the label.',
|
|
74
|
-
'Use multi: true when several options can apply — the TUI opens an interactive toggle list (space toggles a row, a toggles all, enter confirms, esc declines) instead of making the human type numbers.',
|
|
75
|
-
'Set allowOther: false when the authored choices are exhaustive and a free-text answer would be misleading.',
|
|
64
|
+
'Ask the human and wait for the answer — only for a real decision (approval, direction, trade-off), not for information you can find yourself. While waiting, the pane shows as blocked in herdr.',
|
|
65
|
+
'Two shapes. Flat form (no questions key; do not close with ]): {"question":"Store?","options":[{"label":"Redis","description":"In-memory"},{"label":"Postgres"}]}. Batched form: {"questions":[{"question":"Store?","options":[{"label":"Redis"},{"label":"Postgres"}]},{"question":"Region?","options":[{"label":"EU"},{"label":"US"}],"multi":true}]}.',
|
|
76
66
|
].join(' ');
|
|
77
67
|
|
|
78
68
|
export const ASK_PROMPT_GUIDELINES = [
|
|
79
|
-
'Default to action. Ask only when options have materially different tradeoffs the user must decide.',
|
|
80
|
-
'Do NOT include "Other" in options
|
|
81
|
-
'
|
|
82
|
-
'Short option labels; explanatory tradeoffs in description.',
|
|
69
|
+
'Default to action. Ask only when options have materially different tradeoffs the user must decide; group related questions in one call (max 4).',
|
|
70
|
+
'Do NOT include "Other" in options: the UI appends it unless allowOther is false. Set allowOther: false when the choices are exhaustive.',
|
|
71
|
+
'Use multi: true when several options can apply; the human toggles rows and confirms — never ask them to type indices.',
|
|
83
72
|
];
|
|
84
73
|
|
|
85
74
|
export type AskOption = { label: string; description?: string };
|
|
@@ -114,6 +103,8 @@ export type AskDetails = {
|
|
|
114
103
|
export type AskToolResult = {
|
|
115
104
|
content: Array<{ type: 'text'; text: string }>;
|
|
116
105
|
details: AskDetails;
|
|
106
|
+
/** Set on failures so pi/UI do not count a rejected ask as a successful tool call. */
|
|
107
|
+
isError?: boolean;
|
|
117
108
|
};
|
|
118
109
|
|
|
119
110
|
export type AskUi = {
|
|
@@ -144,6 +135,7 @@ function errorResult(text: string, code: string): AskToolResult {
|
|
|
144
135
|
return {
|
|
145
136
|
content: [{ type: 'text', text }],
|
|
146
137
|
details: { answers: [], cancelled: true, error: code },
|
|
138
|
+
isError: true,
|
|
147
139
|
};
|
|
148
140
|
}
|
|
149
141
|
|
|
@@ -233,6 +225,42 @@ function parseQuestion(raw: unknown): { ok: true; question: AskQuestion } | { ok
|
|
|
233
225
|
};
|
|
234
226
|
}
|
|
235
227
|
|
|
228
|
+
type AskParams = Type.Static<typeof ASK_PARAMETERS>;
|
|
229
|
+
|
|
230
|
+
/** Repair the argument spellings models have sent historically, before pi validates them: `options` or
|
|
231
|
+
* `questions` as a JSON string instead of an array. Only strings that parse to arrays are replaced, so
|
|
232
|
+
* anything genuinely malformed still reaches prepareAsk and gets its normal error text. */
|
|
233
|
+
export function prepareAskArguments(args: unknown): AskParams {
|
|
234
|
+
const rec = asRecord(args);
|
|
235
|
+
if (!rec) return {};
|
|
236
|
+
const parseArray = (value: unknown): unknown[] | undefined => {
|
|
237
|
+
if (typeof value !== 'string') return undefined;
|
|
238
|
+
try {
|
|
239
|
+
const parsed: unknown = JSON.parse(value);
|
|
240
|
+
return Array.isArray(parsed) ? parsed : undefined;
|
|
241
|
+
} catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
let changed = false;
|
|
246
|
+
const next: Record<string, unknown> = { ...rec };
|
|
247
|
+
const options = parseArray(rec.options);
|
|
248
|
+
if (options) {
|
|
249
|
+
next.options = options;
|
|
250
|
+
changed = true;
|
|
251
|
+
}
|
|
252
|
+
const questions = parseArray(rec.questions);
|
|
253
|
+
if (questions) {
|
|
254
|
+
next.questions = questions.map((item) => {
|
|
255
|
+
const question = asRecord(item);
|
|
256
|
+
const questionOptions = question ? parseArray(question.options) : undefined;
|
|
257
|
+
return question && questionOptions ? { ...question, options: questionOptions } : item;
|
|
258
|
+
});
|
|
259
|
+
changed = true;
|
|
260
|
+
}
|
|
261
|
+
return (changed ? next : rec) as AskParams;
|
|
262
|
+
}
|
|
263
|
+
|
|
236
264
|
export function prepareAsk(params: unknown): PrepareAskResult {
|
|
237
265
|
const rec = asRecord(params);
|
|
238
266
|
if (!rec) return fail('Error: `question` must be a non-empty string', 'empty_question');
|
|
@@ -312,15 +340,33 @@ function withId(question: AskQuestion, answer: AskAnswer): AskAnswer {
|
|
|
312
340
|
return question.id ? { ...answer, id: question.id } : answer;
|
|
313
341
|
}
|
|
314
342
|
|
|
315
|
-
/** Free-text answer behind the Other row; shared by the dialog and the typed-prompt fallback.
|
|
316
|
-
|
|
343
|
+
/** Free-text answer behind the Other row; shared by the dialog and the typed-prompt fallback. `labels`
|
|
344
|
+
* are the rows toggled when the dialog opened Other — a multi-select custom answer keeps them.
|
|
345
|
+
* Returns 'empty' for a blank answer so the caller can go back to the choices. */
|
|
346
|
+
async function askOtherText(
|
|
347
|
+
ui: AskUi,
|
|
348
|
+
question: AskQuestion,
|
|
349
|
+
labels: readonly string[],
|
|
350
|
+
signal?: AbortSignal,
|
|
351
|
+
): Promise<AskAnswer | 'empty' | undefined> {
|
|
317
352
|
const typed = await ui.input(`${question.question}\n\n${CUSTOM_ANSWER_TITLE}`, '', signal ? { signal } : undefined);
|
|
318
|
-
if (typed == null
|
|
353
|
+
if (typed == null) return undefined;
|
|
354
|
+
const customInput = typed.trim();
|
|
355
|
+
if (!customInput) return 'empty';
|
|
356
|
+
if (question.multi && labels.length > 0) {
|
|
357
|
+
return withId(question, {
|
|
358
|
+
question: question.question,
|
|
359
|
+
kind: 'multi',
|
|
360
|
+
answer: null,
|
|
361
|
+
selected: [...labels],
|
|
362
|
+
customInput,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
319
365
|
return withId(question, {
|
|
320
366
|
question: question.question,
|
|
321
367
|
kind: 'custom',
|
|
322
368
|
answer: OTHER_ANSWER,
|
|
323
|
-
customInput
|
|
369
|
+
customInput,
|
|
324
370
|
});
|
|
325
371
|
}
|
|
326
372
|
|
|
@@ -376,53 +422,71 @@ function parseTypedPicks(
|
|
|
376
422
|
}
|
|
377
423
|
|
|
378
424
|
/** Ask one question, both modes: the pier dialog, then the host's select dialog (single only), then
|
|
379
|
-
* the typed prompt. `ui.custom` is absent (or resolves undefined) in RPC mode, hence every rung.
|
|
425
|
+
* the typed prompt. `ui.custom` is absent (or resolves undefined) in RPC mode, hence every rung.
|
|
426
|
+
* A blank Other answer goes back to the choices instead of declining the question. */
|
|
380
427
|
async function askQuestion(ui: AskUi, question: AskQuestion, signal?: AbortSignal): Promise<AskAnswer | undefined> {
|
|
381
428
|
const { multi } = question;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
question,
|
|
402
|
-
multi
|
|
403
|
-
? { question: question.question, kind: 'multi', answer: null, selected: outcome.labels }
|
|
404
|
-
: { question: question.question, kind: 'option', answer: outcome.labels[0]! },
|
|
429
|
+
const toggleToolsExpanded = ui.getToolsExpanded && ui.setToolsExpanded
|
|
430
|
+
? (): void => ui.setToolsExpanded!(!ui.getToolsExpanded!())
|
|
431
|
+
: undefined;
|
|
432
|
+
let custom = typeof ui.custom === 'function' ? ui.custom : undefined;
|
|
433
|
+
let toggled: string[] = [];
|
|
434
|
+
for (;;) {
|
|
435
|
+
const dialog = custom;
|
|
436
|
+
if (dialog) {
|
|
437
|
+
const outcome = await runSelectDialog(
|
|
438
|
+
dialog,
|
|
439
|
+
question.question,
|
|
440
|
+
{
|
|
441
|
+
options: question.options,
|
|
442
|
+
allowOther: question.allowOther,
|
|
443
|
+
multi,
|
|
444
|
+
...(question.recommended !== undefined ? { recommended: question.recommended } : {}),
|
|
445
|
+
...(toggled.length > 0 ? { initialSelected: question.options.map((option) => toggled.includes(option.label)) } : {}),
|
|
446
|
+
},
|
|
447
|
+
{ ...(signal ? { signal } : {}), ...(toggleToolsExpanded ? { toggleToolsExpanded } : {}) },
|
|
405
448
|
);
|
|
449
|
+
if (outcome?.kind === 'cancel') return undefined;
|
|
450
|
+
if (outcome?.kind === 'other') {
|
|
451
|
+
const answer = await askOtherText(ui, question, outcome.labels, signal);
|
|
452
|
+
if (answer !== 'empty') return answer;
|
|
453
|
+
toggled = outcome.labels;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (outcome?.kind === 'selected') {
|
|
457
|
+
return withId(
|
|
458
|
+
question,
|
|
459
|
+
multi
|
|
460
|
+
? { question: question.question, kind: 'multi', answer: null, selected: outcome.labels }
|
|
461
|
+
: { question: question.question, kind: 'option', answer: outcome.labels[0]! },
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
// The host cannot render custom components (RPC mode): fall through to the typed prompt.
|
|
465
|
+
custom = undefined;
|
|
406
466
|
}
|
|
467
|
+
const lines = optionLines(question);
|
|
468
|
+
const opts = signal ? { signal } : undefined;
|
|
469
|
+
// A multi question has no single-pick host dialog; its typed prompt is always the fallback.
|
|
470
|
+
const select = multi ? undefined : ui.select;
|
|
471
|
+
const hint = multi
|
|
472
|
+
? (question.allowOther ? MULTI_INSTRUCTIONS : MULTI_NO_OTHER_INSTRUCTIONS)
|
|
473
|
+
: NUMBERED_HINT;
|
|
474
|
+
const raw = select
|
|
475
|
+
? await select(question.question, lines, opts)
|
|
476
|
+
: await ui.input(`${question.question}\n\n${lines.join('\n')}\n\n${hint}`, multi ? '1,3' : '1', opts);
|
|
477
|
+
if (raw == null) return undefined;
|
|
478
|
+
const picks = parseTypedPicks(question, lines, raw, select === undefined);
|
|
479
|
+
if (picks === 'other') {
|
|
480
|
+
const answer = await askOtherText(ui, question, [], signal);
|
|
481
|
+
if (answer !== 'empty') return answer;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
return picks?.[0];
|
|
407
485
|
}
|
|
408
|
-
const lines = optionLines(question);
|
|
409
|
-
const opts = signal ? { signal } : undefined;
|
|
410
|
-
// A multi question has no single-pick host dialog; its typed prompt is always the fallback.
|
|
411
|
-
const select = multi ? undefined : ui.select;
|
|
412
|
-
const hint = multi
|
|
413
|
-
? (question.allowOther ? MULTI_INSTRUCTIONS : MULTI_NO_OTHER_INSTRUCTIONS)
|
|
414
|
-
: NUMBERED_HINT;
|
|
415
|
-
const raw = select
|
|
416
|
-
? await select(question.question, lines, opts)
|
|
417
|
-
: await ui.input(`${question.question}\n\n${lines.join('\n')}\n\n${hint}`, multi ? '1,3' : '1', opts);
|
|
418
|
-
if (raw == null) return undefined;
|
|
419
|
-
const picks = parseTypedPicks(question, lines, raw, select === undefined);
|
|
420
|
-
if (picks === 'other') return askOtherText(ui, question, signal);
|
|
421
|
-
return picks?.[0];
|
|
422
486
|
}
|
|
423
487
|
|
|
424
488
|
export function formatAskContent(details: AskDetails): string {
|
|
425
|
-
if (details.
|
|
489
|
+
if (details.answers.length === 0) return DECLINE_TEXT;
|
|
426
490
|
const entries = details.answers.map((answer) => {
|
|
427
491
|
if (answer.kind === 'custom' && answer.answer === OTHER_ANSWER) {
|
|
428
492
|
const notes = answer.customInput ? ` user notes: ${answer.customInput}` : '';
|
|
@@ -430,11 +494,15 @@ export function formatAskContent(details: AskDetails): string {
|
|
|
430
494
|
}
|
|
431
495
|
if (answer.kind === 'multi') {
|
|
432
496
|
const labels = answer.selected?.length ? answer.selected.join(', ') : '(none)';
|
|
433
|
-
|
|
497
|
+
const notes = answer.customInput ? ` user notes: ${answer.customInput}` : '';
|
|
498
|
+
return `"${answer.question}"="${labels}"${notes}`;
|
|
434
499
|
}
|
|
435
500
|
return `"${answer.question}"="${answer.answer ?? ''}"`;
|
|
436
501
|
});
|
|
437
|
-
|
|
502
|
+
const answered = `User has answered your questions: ${entries.join(', ')}.`;
|
|
503
|
+
return details.cancelled
|
|
504
|
+
? `${answered} The user declined the remaining questions — continue with these answers and your best judgment.`
|
|
505
|
+
: `${answered} You can now continue with the user's answers in mind.`;
|
|
438
506
|
}
|
|
439
507
|
|
|
440
508
|
export async function runAsk(spec: AskSpec, ui: AskUi, signal?: AbortSignal): Promise<AskToolResult> {
|