@capdiem/pi-ask-user 0.1.3 → 0.2.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 +24 -6
- package/index.min.js +10 -9
- package/index.min.js.map +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-ask-user
|
|
2
2
|
|
|
3
3
|
An interactive `ask_user` form tool for the [Pi coding agent](https://pi.dev/).
|
|
4
4
|
|
|
@@ -76,8 +76,16 @@ The top-level call accepts an optional `numbered` flag (default `false`):
|
|
|
76
76
|
|
|
77
77
|
## Wiring it into grilling skills
|
|
78
78
|
|
|
79
|
-
`ask_user` ships `promptGuidelines`
|
|
80
|
-
|
|
79
|
+
`ask_user` ships strengthened `promptGuidelines` (since 0.2.0) that make the form
|
|
80
|
+
**mandatory** in grilling / domain-modeling / design-interview question rounds:
|
|
81
|
+
when a workflow presents question rounds as numbered plain-text blocks
|
|
82
|
+
(`Q1..QN`, `❓`, `recommended answer`), the model must call `ask_user` instead of
|
|
83
|
+
typing them out. The guidelines name the grilling family explicitly plus a
|
|
84
|
+
generic "成轮提问 / design-interview" fallback, so no third-party skill edits
|
|
85
|
+
are needed. See
|
|
86
|
+
[`docs/adr/0001-ask-user-triggering-metadata-first.md`](../../docs/adr/0001-ask-user-triggering-metadata-first.md).
|
|
87
|
+
|
|
88
|
+
It maps naturally onto [Matt Pocock's `grilling`
|
|
81
89
|
skill](https://github.com/mattpocock/skills) format:
|
|
82
90
|
|
|
83
91
|
| grilling skill | `ask_user` |
|
|
@@ -87,10 +95,20 @@ skill](https://github.com/mattpocock/skills) format:
|
|
|
87
95
|
| `➡️ <recommended answer>` | `recommendation` hint (marked `★` on the matching option) |
|
|
88
96
|
| free-form asks | `type: "text"` questions |
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
## Debugging trigger behavior
|
|
99
|
+
|
|
100
|
+
Pass `--ask-user-debug` to log one line per turn recording whether a question
|
|
101
|
+
round happened and whether it used `ask_user`:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pi --ask-user-debug
|
|
105
|
+
```
|
|
91
106
|
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
Example output: `[pi-ask-user] turn 3: question round via plain text
|
|
108
|
+
(wrotePlain=true, clean=false)`. This feeds the escalation gate (ADR-0001): if
|
|
109
|
+
repeated multi-scenario runs show most question rounds bypassing `ask_user`, the
|
|
110
|
+
metadata-first approach is judged insufficient and per-turn injection is
|
|
111
|
+
reconsidered.
|
|
94
112
|
|
|
95
113
|
## License
|
|
96
114
|
|
package/index.min.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import{StringEnum as
|
|
2
|
-
`)}function
|
|
1
|
+
import{StringEnum as Yj}from"@earendil-works/pi-ai";import{Editor as Zj,Key as M,matchesKey as Q,Text as t,visibleWidth as $j,wrapTextWithAnsi as e}from"@earendil-works/pi-tui";import{Type as C}from"typebox";var Jj=[/\bQ\s?\d+\b/i,/❓/u,/recommended answer|推荐答案/i,/请回答|请选择|你决定|你的选择|你更倾向|选一个/i];function Xj(G){if(!G)return!1;return Jj.some((z)=>z.test(G))}function i(G){if(!G||typeof G!=="object")return"";let z=G.content;if(typeof z==="string")return z;if(!Array.isArray(z))return"";return z.filter((H)=>typeof H==="object"&&H!==null&&H.type==="text").map((H)=>H.text??"").join(`
|
|
2
|
+
`)}function s(G,z){let H=Xj(G),j=z||H;return{isQuestionRound:j,usedAskUser:z,wrotePlainQuestions:H,handledCleanly:j&&z&&!H}}var Fj=C.Object({value:C.String({description:"Value returned to the LLM when this option is selected"}),label:C.String({description:"Display label shown to the user"}),description:C.Optional(C.String({description:"Optional description shown under the label"}))}),Nj=C.Object({id:C.String({description:"Unique question id (e.g. q1, q2, scope)"}),title:C.Optional(C.String({description:"Optional short title shown after the question number, e.g. 'Scope'"})),prompt:C.String({description:"Full question text to display"}),type:Yj(["choice","text"],{description:"'choice' for an options list, 'text' for free-form input"}),options:C.Optional(C.Array(Fj,{description:"Options for type=choice (required for choice)"})),allowOther:C.Optional(C.Boolean({description:"Choice questions: allow free-text 'Type something' (default true)"})),recommendation:C.Optional(C.String({description:"Your recommended answer (the grilling '➡️ recommended answer'). When it matches an option's value or label, that option is highlighted in the form."}))}),_j=C.Object({questions:C.Array(Nj,{description:"One or more questions to ask the user in a single interactive form",minItems:1,maxItems:10}),numbered:C.Optional(C.Boolean({description:"Prefix each question with its number (Q1, Q2…) in grilling style. Default: false."}))});function Bj(G){return G.questions.map((z,H)=>({...z,allowOther:z.allowOther!==!1,options:z.type==="choice"?z.options??[]:void 0,number:H+1,numbered:G.numbered===!0}))}function I(G){if(G.numbered)return`Q${G.number}${G.title?` - ${G.title}`:""}`;return G.title??""}function Cj(G,z){let H=G.recommendation?.trim().toLowerCase();if(!H)return!1;let j=(z.value??"").trim().toLowerCase(),X=(z.label??"").trim().toLowerCase();if(j&&H===j)return!0;if(X&&H===X)return!0;if(j.length>=3&&H.includes(j))return!0;if(X.length>=3&&H.includes(X))return!0;if(H.length>=3){if(j&&j.includes(H))return!0;if(X&&X.includes(H))return!0}return!1}function l(G){if(!G.recommendation||G.type!=="choice")return-1;let z=G.options??[];for(let H=0;H<z.length;H++)if(Cj(G,z[H]))return H;return-1}function m(G,z){let H=G.trim();if(z)for(let X of[z.label,z.value]){let B=X?.trim();if(!B)continue;if(H.toLowerCase().startsWith(B.toLowerCase())){H=H.slice(B.length).trimStart();break}}let j=H.match(/[\u2014\u2013]| - |-/);if(j&&typeof j.index==="number")H=H.slice(j.index+j[0].length).replace(/^[\s::,,、.…-]+/,"");return H.trim()||G.trim()}function Vj(G,z,H=!0){return{content:[{type:"text",text:G}],details:{questions:z,answers:[],cancelled:H}}}function jj(G,z){return z.map((j)=>{let X=G.find((Y)=>Y.id===j.id),B=X?I(X)||`Q${X.number}`:j.id;if(j.wasCustom)return`${B}: user wrote: ${j.label}`;let V=j.index?` ${j.index}.`:"";return`${B}: user selected:${V} ${j.label}`}).join(`
|
|
3
|
+
`)}function Dj(G){return{borderColor:(z)=>G.fg("accent",z),selectList:{selectedPrefix:(z)=>G.fg("accent",z),selectedText:(z)=>G.fg("accent",z),description:(z)=>G.fg("muted",z),scrollInfo:(z)=>G.fg("dim",z),noMatch:(z)=>G.fg("warning",z)}}}async function Ej(G,z){return G.ui.custom((H,j,X,B)=>{let V=z.length>1,Y=z.length,_=0,D=0,J=!1,Z=null,O,L=new Map,P=new Zj(H,Dj(j));function A(){O=void 0,H.requestRender()}function W($){B({questions:z,answers:Array.from(L.values()),cancelled:$})}function T(){return z[_]}function u(){let $=T();if(!$||$.type!=="choice")return[];let F=[...$.options??[]];if($.allowOther)F.push({value:"__other__",label:"Type something.",isOther:!0});return F}function q(){return z.every(($)=>L.has($.id))}function o(){if(!V){W(!1);return}if(_<Y-1)_++;else _=Y;D=0,J=!1,Z=null,A()}function d($,F,k,g,E){L.set($,{id:$,value:F,label:k,wasCustom:g,index:E})}function r($){J=!0,Z=$,P.setText(""),A()}P.onSubmit=($)=>{if(!Z)return;let F=$.trim()||"(no response)";d(Z,F,F,!0),J=!1,Z=null,P.setText(""),o()};function zj($){if(J){if(Q($,M.escape)){J=!1,Z=null,P.setText(""),A();return}P.handleInput($),A();return}if(V){if(Q($,M.tab)||Q($,M.right)){_=(_+1)%(Y+1),D=0,A();return}if(Q($,M.shift("tab"))||Q($,M.left)){_=(_-1+Y+1)%(Y+1),D=0,A();return}}let F=T();if(_===Y){if(Q($,M.enter)&&q())W(!1);else if(Q($,M.escape))W(!0);return}if(!F)return;if(F.type==="text"){if(Q($,M.enter))r(F.id);else if(Q($,M.escape))W(!0);return}let k=u();if(Q($,M.up)){D=Math.max(0,D-1),A();return}if(Q($,M.down)){D=Math.min(k.length-1,D+1),A();return}if(Q($,M.enter)){let g=k[D];if(!g)return;if(g.isOther){r(F.id);return}d(F.id,g.value,g.label,!1,D+1),o();return}if(Q($,M.escape))W(!0)}function Gj($){if(O)return O;let F=[],k=Math.max(1,$);function g(N){F.push(...e(N,k))}function E(N,S){let K=$j(N);if(K>=k){g(N+S);return}let R=e(S,k-K),v=" ".repeat(K);for(let U=0;U<R.length;U++)F.push(`${U===0?N:v}${R[U]}`)}function n(N){let S=l(N)>=0,K=I(N),R=K?`${j.fg("accent",j.bold(`${K}:`))} ${j.fg("text",N.prompt)}`:j.fg("text",N.prompt);if(E(" ",R),N.recommendation&&!S)F.push(""),E(" ",j.fg("dim",`Recommended: ${m(N.recommendation)}`));F.push("")}if(F.push(j.fg("accent","─".repeat(k))),V){let N=["← "];for(let U=0;U<z.length;U++){let y=U===_,f=L.has(z[U].id),c=z[U].title??`Q${U+1}`,b=f?"■":"□",h=f?"success":"muted",x=` ${b} ${c} `,p=y?j.bg("selectedBg",j.fg("text",x)):j.fg(h,x);N.push(`${p} `)}let S=q(),K=_===Y,R=" ✓ Submit ",v=K?j.bg("selectedBg",j.fg("text",R)):j.fg(S?"success":"dim",R);N.push(`${v} →`),E(" ",N.join("")),F.push("")}function a(){let N=u(),S=T(),K=S?l(S):-1;for(let R=0;R<N.length;R++){let v=N[R],U=R===D,y=v.isOther===!0,f=!y&&R===K,c=U?j.fg("accent","> "):" ",b=`${R+1}. ${f?"★ ":""}${v.label}${y&&J?" ✎":""}`,h=f?j.bold(b):b,x=U||y&&J?"accent":"text";if(E(c,j.fg(x,h)),f&&S?.recommendation){let p=m(S.recommendation,v),Hj=v.description?j.fg("muted",v.description):"";E(" ",Hj+j.bold(`(推荐:${p})`))}else if(v.description)E(" ",j.fg("muted",v.description))}}let w=T();if(J&&w){if(n(w),w.type==="choice")a();F.push(""),E(" ",j.fg("muted","Your answer:"));for(let N of P.render(Math.max(1,k-2)))F.push(` ${N}`);F.push(""),E(" ",j.fg("dim","Enter to submit • Esc to cancel"))}else if(_===Y){E(" ",j.fg("accent",j.bold("Ready to submit"))),F.push("");for(let N of z){let S=L.get(N.id);if(S){let K=S.wasCustom?"(wrote) ":"",R=`${j.fg("muted",`${I(N)}: `)}${j.fg("text",K+S.label)}`;E(" ",R)}}if(F.push(""),q())E(" ",j.fg("success","Press Enter to submit"));else{let N=z.filter((S)=>!L.has(S.id)).map((S)=>I(S)).join(", ");E(" ",j.fg("warning",`Unanswered: ${N}`))}}else if(w)if(n(w),w.type==="choice")a();else{let N=L.get(w.id);if(E(" ",j.fg("muted","Free-form answer")),F.push(""),N)E(" ",j.fg("text",` ${N.label}`)),F.push(""),E(" ",j.fg("dim","Enter to edit • Esc cancel"));else E(" ",j.fg("dim","Press Enter to type your answer"))}if(F.push(""),!J){let N=V?"Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel":"↑↓ select • Enter confirm • Esc cancel";E(" ",j.fg("dim",N))}return F.push(j.fg("accent","─".repeat(k))),O=F,F}return{render:Gj,invalidate:()=>{O=void 0},handleInput:zj}})}async function Sj(G,z){let H=[],j=!1;for(let X of z)if(X.type==="choice"){let B=X.options??[],V=l(X),Y=B.map((Z,O)=>({index:O,label:O===V&&X.recommendation?`${Z.label}(推荐:${m(X.recommendation,Z)})`:Z.label}));if(X.allowOther)Y.push({index:-1,label:"Type something..."});let _=I(X),D=_?`${_}: ${X.prompt}`:X.prompt,J=await G.ui.select(D,Y.map((Z)=>Z.label));if(J===void 0){j=!0;break}if(J==="Type something..."){let Z=await G.ui.input(X.prompt,"Type your answer");if(Z===void 0){j=!0;break}H.push({id:X.id,value:Z,label:Z,wasCustom:!0})}else{let Z=Y.find((P)=>P.label===J),O=Z?Z.index:-1,L=O>=0?B[O]:void 0;H.push({id:X.id,value:L?.value??J,label:L?.label??J,wasCustom:!1,index:O>=0?O+1:void 0})}}else{let B=await G.ui.input(X.prompt,I(X)||X.prompt);if(B===void 0){j=!0;break}H.push({id:X.id,value:B,label:B,wasCustom:!0})}return{questions:z,answers:H,cancelled:j}}function Oj(G){G.registerTool({name:"ask_user",label:"Ask User",description:"Ask the user one or more questions as an interactive form (choice options or free-text). Use it whenever you need the user's decision, preference, or input to continue. In grilling, domain-modeling, or design-interview workflows that present question rounds "+"as numbered plain-text blocks (Q1..QN, ❓, 'recommended answer'), you MUST call ask_user "+"to present those questions as a form instead of typing them out. Each question may include a 'recommendation'; when it matches one of the options, that option is highlighted.",promptSnippet:"Present user questions as an interactive form (mandatory for grilling/design question rounds)",promptGuidelines:["In grilling, domain-modeling, or any design-interview workflow (e.g. the grilling / grill-with-docs / domain-modeling skills) that presents question rounds as numbered plain-text blocks (Q1..QN, ❓, 'recommended answer'), you MUST call ask_user to present each round as an interactive form — do not write the questions out as plain text.","Use ask_user whenever you need the user's decision, preference, or free-text input to continue a task, instead of typing the questions inline.","When a single turn has multiple related questions (e.g. a grilling round's frontier), pass them all in one ask_user call — one question per entry, with type 'choice' or 'text'.","For each ask_user question you can include a 'recommendation' with your recommended answer. When it matches one of the options, that option is highlighted; otherwise it is shown as a hint under the question.","For grilling-style rounds set numbered: true so the questions are prefixed Q1/Q2. For ordinary questions omit it — the form then shows just the prompt (plus an optional title).","ask_user works in TUI mode (full form) and RPC mode (sequential dialogs). In print/json mode it returns the questions as text, so reserve it for interactive sessions where a form can render.","If ask_user reports 'cancelled', stop and let the user redirect instead of re-asking the same questions."],parameters:_j,executionMode:"sequential",async execute(j,X,B,V,Y){if(B?.aborted)return Vj("Cancelled",[],!0);let _=Bj(X);if(Y.mode==="tui"){let J=await Ej(Y,_);if(J.cancelled)return{content:[{type:"text",text:"User cancelled the questions"}],details:J};return{content:[{type:"text",text:jj(_,J.answers)}],details:J}}if(Y.mode==="rpc"){let J=await Sj(Y,_);if(J.cancelled)return{content:[{type:"text",text:"User cancelled the questions"}],details:J};return{content:[{type:"text",text:jj(_,J.answers)}],details:J}}return{content:[{type:"text",text:`Interactive form unavailable in this mode. Ask the user the following questions as plain text (numbered Q1..QN):
|
|
3
4
|
|
|
4
|
-
`+
|
|
5
|
-
Recommended: ${
|
|
6
|
-
Options: ${(
|
|
5
|
+
`+_.map((J)=>{let Z=J.recommendation?`
|
|
6
|
+
Recommended: ${J.recommendation}`:"",O=J.type==="choice"?`
|
|
7
|
+
Options: ${(J.options??[]).map((P)=>`${P.label}`).join(" | ")}`:"",L=I(J);return`${L?`${L}: `:""}${J.prompt}${O}${Z}`}).join(`
|
|
7
8
|
|
|
8
|
-
`)}],details:{questions:
|
|
9
|
-
`);return Y.setText(
|
|
10
|
-
`)),Y}})}export{
|
|
9
|
+
`)}],details:{questions:_,answers:[],cancelled:!1}}},renderCall(j,X,B){let V=B.lastComponent??new t("",0,0),Y=Array.isArray(j.questions)?j.questions:[],_=Y.map((J,Z)=>J.title?`Q${Z+1} - ${J.title}`:J.id||`Q${Z+1}`).join(", "),D=X.fg("toolTitle",X.bold("ask_user "));if(D+=X.fg("muted",`${Y.length} question${Y.length!==1?"s":""}`),_)D+=X.fg("dim",` (${_})`);return V.setText(D),V},renderResult(j,X,B,V){let Y=V.lastComponent??new t("",0,0),_=j.details;if(!_){let J=j.content.filter((Z)=>Z.type==="text").map((Z)=>Z.text).join(`
|
|
10
|
+
`);return Y.setText(B.fg("warning",J||"ask_user")),Y}if(_.cancelled)return Y.setText(B.fg("warning","Cancelled")),Y;let D=_.answers.map((J)=>{let Z=_.questions.find((P)=>P.id===J.id),O=Z?I(Z)||`Q${Z.number}`:J.id;if(J.wasCustom)return`${B.fg("success","✓ ")}${B.fg("accent",O)}: ${B.fg("muted","(wrote) ")}${J.label}`;let L=J.index?`${J.index}. ${J.label}`:J.label;return`${B.fg("success","✓ ")}${B.fg("accent",O)}: ${L}`});return Y.setText(D.join(`
|
|
11
|
+
`)),Y}});let z=!1;G.registerFlag("ask-user-debug",{description:"Log per-turn question-round diagnostics for ask_user triggering",type:"boolean",default:!1});let H=()=>G.getFlag("ask-user-debug")===!0;G.on("turn_start",()=>{if(H())z=!1}),G.on("tool_call",(j,X)=>{if(H()&&j.toolName==="ask_user")z=!0}),G.on("turn_end",(j,X)=>{if(!H())return;let B=i(j.message),V=s(B,z);if(!V.isQuestionRound)return;let Y=V.usedAskUser?"ask_user":"plain text";console.log(`[pi-ask-user] turn ${j.turnIndex}: question round via ${Y} (wrotePlain=${V.wrotePlainQuestions}, clean=${V.handledCleanly})`)})}export{Oj as default};
|
|
11
12
|
|
|
12
|
-
//# debugId=
|
|
13
|
+
//# debugId=F81601C4D787F3A264756E2164756E21
|
|
13
14
|
//# sourceMappingURL=index.min.js.map
|
package/index.min.js.map
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../extensions/pi-ask-user/index.ts"],
|
|
3
|
+
"sources": ["../../extensions/pi-ask-user/index.ts", "../../extensions/pi-ask-user/diagnostic.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Ask User Questions - interactive question form tool\n *\n * Lets the LLM ask the user one or more questions as an interactive form\n * (choice + free-text), instead of dumping plain-text \"Q1..QN\" blocks.\n *\n * Mode behavior:\n * - TUI mode: full-screen tabbed form via ctx.ui.custom()\n * - RPC mode: per-question select/input dialogs over the extension UI protocol\n * - print/json mode: structured fallback so the LLM can ask in plain text\n *\n * Each question carries a \"recommendation\" hint (the grilling skill's\n * \"➡️ recommended answer\"). When it matches one of a choice question's\n * options, it is highlighted on that option; otherwise it is shown dimmed\n * under the prompt.\n */\n\nimport type { ExtensionAPI, ExtensionContext, Theme } from \"@earendil-works/pi-coding-agent\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport {\n Editor,\n type EditorTheme,\n Key,\n matchesKey,\n Text,\n visibleWidth,\n wrapTextWithAnsi,\n} from \"@earendil-works/pi-tui\";\nimport { Type } from \"typebox\";\n\n// ---------- Types ----------\n\ninterface QuestionOption {\n value: string;\n label: string;\n description?: string;\n}\n\ninterface Question {\n id: string;\n title?: string;\n prompt: string;\n type: \"choice\" | \"text\";\n options?: QuestionOption[];\n allowOther: boolean;\n recommendation?: string;\n number: number;\n numbered: boolean;\n}\n\ninterface Answer {\n id: string;\n value: string;\n label: string;\n wasCustom: boolean;\n index?: number;\n}\n\ninterface AskUserResult {\n questions: Question[];\n answers: Answer[];\n cancelled: boolean;\n}\n\n// ---------- Schema ----------\n\nconst QuestionOptionSchema = Type.Object({\n value: Type.String({ description: \"Value returned to the LLM when this option is selected\" }),\n label: Type.String({ description: \"Display label shown to the user\" }),\n description: Type.Optional(\n Type.String({ description: \"Optional description shown under the label\" }),\n ),\n});\n\nconst QuestionSchema = Type.Object({\n id: Type.String({ description: \"Unique question id (e.g. q1, q2, scope)\" }),\n title: Type.Optional(\n Type.String({ description: \"Optional short title shown after the question number, e.g. 'Scope'\" }),\n ),\n prompt: Type.String({ description: \"Full question text to display\" }),\n type: StringEnum([\"choice\", \"text\"] as const, {\n description: \"'choice' for an options list, 'text' for free-form input\",\n }),\n options: Type.Optional(\n Type.Array(QuestionOptionSchema, { description: \"Options for type=choice (required for choice)\" }),\n ),\n allowOther: Type.Optional(\n Type.Boolean({ description: \"Choice questions: allow free-text 'Type something' (default true)\" }),\n ),\n recommendation: Type.Optional(\n Type.String({\n description:\n \"Your recommended answer (the grilling '➡️ recommended answer'). When it matches an option's value or label, that option is highlighted in the form.\",\n }),\n ),\n});\n\nconst AskUserParams = Type.Object({\n questions: Type.Array(QuestionSchema, {\n description: \"One or more questions to ask the user in a single interactive form\",\n minItems: 1,\n maxItems: 10,\n }),\n numbered: Type.Optional(\n Type.Boolean({\n description: \"Prefix each question with its number (Q1, Q2…) in grilling style. Default: false.\",\n }),\n ),\n});\n\n// ---------- Helpers ----------\n\n/** Raw question shape as accepted by the tool (optional fields not yet normalized). */\ninterface RawQuestion {\n id: string;\n title?: string;\n prompt: string;\n type: \"choice\" | \"text\";\n options?: QuestionOption[];\n allowOther?: boolean;\n recommendation?: string;\n}\n\nfunction defaultQuestions(params: {\n questions: RawQuestion[];\n numbered?: boolean;\n}): Question[] {\n return params.questions.map((q, i) => ({\n ...q,\n allowOther: q.allowOther !== false,\n options: q.type === \"choice\" ? q.options ?? [] : undefined,\n number: i + 1,\n numbered: params.numbered === true,\n }));\n}\n\n/**\n * Question label: \"Q1 - Scope\" when numbered, \"Scope\" when not. Empty string\n * when not numbered and no title is set.\n */\nfunction questionLabel(q: Pick<Question, \"number\" | \"numbered\" | \"title\">): string {\n if (q.numbered) return `Q${q.number}${q.title ? ` - ${q.title}` : \"\"}`;\n return q.title ?? \"\";\n}\n\n/** Whether a question's recommendation points at a specific option (heuristic match). */\nfunction recommendationMatch(q: Question, opt: QuestionOption): boolean {\n const rec = q.recommendation?.trim().toLowerCase();\n if (!rec) return false;\n const value = (opt.value ?? \"\").trim().toLowerCase();\n const label = (opt.label ?? \"\").trim().toLowerCase();\n if (value && rec === value) return true;\n if (label && rec === label) return true;\n if (value.length >= 3 && rec.includes(value)) return true;\n if (label.length >= 3 && rec.includes(label)) return true;\n if (rec.length >= 3) {\n if (value && value.includes(rec)) return true;\n if (label && label.includes(rec)) return true;\n }\n return false;\n}\n\n/**\n * Index of the option a question's recommendation points at, or -1 when the\n * recommendation is absent or matches no option. Returns the first match.\n */\nfunction recommendedIndex(q: Question): number {\n if (!q.recommendation || q.type !== \"choice\") return -1;\n const opts = q.options ?? [];\n for (let i = 0; i < opts.length; i++) {\n if (recommendationMatch(q, opts[i])) return i;\n }\n return -1;\n}\n\n/**\n * Reduce a recommendation to its \"detail\" for display.\n * Grill recommendation shape is \"<标题> - <详情>\"; we take only the <详情>\n * after the first dash separator (em/en dash, spaced hyphen, fullwidth\n * hyphen-minus), so a leading echo of the option title is dropped even when\n * the title carries extra text like \"(现状)\". A leading echo of the option\n * label/value is also stripped for the no-dash case.\n */\nfunction stripRecommendationTitle(rec: string, opt?: QuestionOption): string {\n let cleaned = rec.trim();\n if (opt) {\n for (const token of [opt.label, opt.value]) {\n const t = token?.trim();\n if (!t) continue;\n if (cleaned.toLowerCase().startsWith(t.toLowerCase())) {\n cleaned = cleaned.slice(t.length).trimStart();\n break;\n }\n }\n }\n const m = cleaned.match(/[\\u2014\\u2013]| - |-/);\n if (m && typeof m.index === \"number\") {\n cleaned = cleaned\n .slice(m.index + m[0].length)\n .replace(/^[\\s::,,、.…-]+/, \"\");\n }\n return cleaned.trim() || rec.trim();\n}\n\nfunction textResult(\n message: string,\n questions: Question[],\n cancelled = true,\n): { content: { type: \"text\"; text: string }[]; details: AskUserResult } {\n return {\n content: [{ type: \"text\", text: message }],\n details: { questions, answers: [], cancelled },\n };\n}\n\nfunction formatAnswers(questions: Question[], answers: Answer[]): string {\n const lines = answers.map((a) => {\n const q = questions.find((x) => x.id === a.id);\n const label = q ? questionLabel(q) || `Q${q.number}` : a.id;\n if (a.wasCustom) return `${label}: user wrote: ${a.label}`;\n const idx = a.index ? ` ${a.index}.` : \"\";\n return `${label}: user selected:${idx} ${a.label}`;\n });\n return lines.join(\"\\n\");\n}\n\n// ---------- TUI form ----------\n\nfunction editorTheme(theme: Theme): EditorTheme {\n return {\n borderColor: (s) => theme.fg(\"accent\", s),\n selectList: {\n selectedPrefix: (t) => theme.fg(\"accent\", t),\n selectedText: (t) => theme.fg(\"accent\", t),\n description: (t) => theme.fg(\"muted\", t),\n scrollInfo: (t) => theme.fg(\"dim\", t),\n noMatch: (t) => theme.fg(\"warning\", t),\n },\n };\n}\n\nasync function presentTuiForm(\n ctx: ExtensionContext,\n questions: Question[],\n): Promise<AskUserResult> {\n return ctx.ui.custom<AskUserResult>((tui, theme, _kb, done) => {\n const isMulti = questions.length > 1;\n const submitTab = questions.length;\n\n let currentTab = 0;\n let optionIndex = 0;\n let inputMode = false;\n let inputQuestionId: string | null = null;\n let cachedLines: string[] | undefined;\n const answers = new Map<string, Answer>();\n\n const editor = new Editor(tui, editorTheme(theme));\n\n // ---------- helpers ----------\n\n function refresh() {\n cachedLines = undefined;\n tui.requestRender();\n }\n\n function submit(cancelled: boolean) {\n done({ questions, answers: Array.from(answers.values()), cancelled });\n }\n\n function currentQuestion(): Question | undefined {\n return questions[currentTab];\n }\n\n function currentOptions(): Array<QuestionOption & { isOther?: boolean }> {\n const q = currentQuestion();\n if (!q || q.type !== \"choice\") return [];\n const opts: Array<QuestionOption & { isOther?: boolean }> = [...(q.options ?? [])];\n if (q.allowOther) {\n opts.push({ value: \"__other__\", label: \"Type something.\", isOther: true });\n }\n return opts;\n }\n\n function allAnswered(): boolean {\n return questions.every((q) => answers.has(q.id));\n }\n\n function advanceAfterAnswer() {\n if (!isMulti) {\n submit(false);\n return;\n }\n if (currentTab < submitTab - 1) {\n currentTab++;\n } else {\n currentTab = submitTab;\n }\n optionIndex = 0;\n inputMode = false;\n inputQuestionId = null;\n refresh();\n }\n\n function saveAnswer(\n questionId: string,\n value: string,\n label: string,\n wasCustom: boolean,\n index?: number,\n ) {\n answers.set(questionId, { id: questionId, value, label, wasCustom, index });\n }\n\n function openInput(questionId: string) {\n inputMode = true;\n inputQuestionId = questionId;\n editor.setText(\"\");\n refresh();\n }\n\n editor.onSubmit = (value) => {\n if (!inputQuestionId) return;\n const trimmed = value.trim() || \"(no response)\";\n saveAnswer(inputQuestionId, trimmed, trimmed, true);\n inputMode = false;\n inputQuestionId = null;\n editor.setText(\"\");\n advanceAfterAnswer();\n };\n\n // ---------- input ----------\n\n function handleInput(data: string) {\n if (inputMode) {\n if (matchesKey(data, Key.escape)) {\n inputMode = false;\n inputQuestionId = null;\n editor.setText(\"\");\n refresh();\n return;\n }\n editor.handleInput(data);\n refresh();\n return;\n }\n\n // Tab navigation (multi-question only)\n if (isMulti) {\n if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {\n currentTab = (currentTab + 1) % (submitTab + 1);\n optionIndex = 0;\n refresh();\n return;\n }\n if (matchesKey(data, Key.shift(\"tab\")) || matchesKey(data, Key.left)) {\n currentTab = (currentTab - 1 + submitTab + 1) % (submitTab + 1);\n optionIndex = 0;\n refresh();\n return;\n }\n }\n\n const q = currentQuestion();\n\n // Submit tab\n if (currentTab === submitTab) {\n if (matchesKey(data, Key.enter) && allAnswered()) {\n submit(false);\n } else if (matchesKey(data, Key.escape)) {\n submit(true);\n }\n return;\n }\n\n if (!q) return;\n\n if (q.type === \"text\") {\n if (matchesKey(data, Key.enter)) {\n openInput(q.id);\n } else if (matchesKey(data, Key.escape)) {\n submit(true);\n }\n return;\n }\n\n // Choice navigation\n const opts = currentOptions();\n if (matchesKey(data, Key.up)) {\n optionIndex = Math.max(0, optionIndex - 1);\n refresh();\n return;\n }\n if (matchesKey(data, Key.down)) {\n optionIndex = Math.min(opts.length - 1, optionIndex + 1);\n refresh();\n return;\n }\n if (matchesKey(data, Key.enter)) {\n const opt = opts[optionIndex];\n if (!opt) return;\n if (opt.isOther) {\n openInput(q.id);\n return;\n }\n saveAnswer(q.id, opt.value, opt.label, false, optionIndex + 1);\n advanceAfterAnswer();\n return;\n }\n if (matchesKey(data, Key.escape)) {\n submit(true);\n }\n }\n\n // ---------- render ----------\n\n function render(width: number): string[] {\n if (cachedLines) return cachedLines;\n\n const lines: string[] = [];\n const renderWidth = Math.max(1, width);\n\n function addWrapped(text: string) {\n lines.push(...wrapTextWithAnsi(text, renderWidth));\n }\n\n function addWrappedWithPrefix(prefix: string, text: string) {\n const prefixWidth = visibleWidth(prefix);\n if (prefixWidth >= renderWidth) {\n addWrapped(prefix + text);\n return;\n }\n const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);\n const continuationPrefix = \" \".repeat(prefixWidth);\n for (let i = 0; i < wrapped.length; i++) {\n lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`);\n }\n }\n\n function renderPromptAndRecommendation(q: Question) {\n const recOnOption = recommendedIndex(q) >= 0;\n const label = questionLabel(q);\n const head = label\n ? `${theme.fg(\"accent\", theme.bold(`${label}:`))} ${theme.fg(\"text\", q.prompt)}`\n : theme.fg(\"text\", q.prompt);\n addWrappedWithPrefix(\" \", head);\n if (q.recommendation && !recOnOption) {\n lines.push(\"\");\n addWrappedWithPrefix(\n \" \",\n theme.fg(\"dim\", `Recommended: ${stripRecommendationTitle(q.recommendation)}`),\n );\n }\n lines.push(\"\");\n }\n\n lines.push(theme.fg(\"accent\", \"─\".repeat(renderWidth)));\n\n // Tab bar (multi-question only)\n if (isMulti) {\n const tabs: string[] = [\"← \"];\n for (let i = 0; i < questions.length; i++) {\n const isActive = i === currentTab;\n const isAnswered = answers.has(questions[i].id);\n const lbl = questions[i].title ?? `Q${i + 1}`;\n const box = isAnswered ? \"■\" : \"□\";\n const color = isAnswered ? \"success\" : \"muted\";\n const text = ` ${box} ${lbl} `;\n const styled = isActive\n ? theme.bg(\"selectedBg\", theme.fg(\"text\", text))\n : theme.fg(color, text);\n tabs.push(`${styled} `);\n }\n const canSubmit = allAnswered();\n const isSubmitTab = currentTab === submitTab;\n const submitText = \" ✓ Submit \";\n const submitStyled = isSubmitTab\n ? theme.bg(\"selectedBg\", theme.fg(\"text\", submitText))\n : theme.fg(canSubmit ? \"success\" : \"dim\", submitText);\n tabs.push(`${submitStyled} →`);\n addWrappedWithPrefix(\" \", tabs.join(\"\"));\n lines.push(\"\");\n }\n\n // Render an option list, highlighting the recommended option if any.\n function renderOptions() {\n const opts = currentOptions();\n const q = currentQuestion();\n const recIdx = q ? recommendedIndex(q) : -1;\n for (let i = 0; i < opts.length; i++) {\n const opt = opts[i];\n const selected = i === optionIndex;\n const isOther = opt.isOther === true;\n const recommended = !isOther && i === recIdx;\n const prefix = selected ? theme.fg(\"accent\", \"> \") : \" \";\n const labelBase = `${i + 1}. ${recommended ? \"★ \" : \"\"}${opt.label}${isOther && inputMode ? \" ✎\" : \"\"}`;\n const label = recommended ? theme.bold(labelBase) : labelBase;\n const color = selected || (isOther && inputMode) ? \"accent\" : \"text\";\n addWrappedWithPrefix(prefix, theme.fg(color, label));\n if (recommended && q?.recommendation) {\n // Recommended option: description (muted) then the recommendation\n // detail (default + bold) wrapped as (推荐:<详情>), same line.\n const detail = stripRecommendationTitle(q.recommendation, opt);\n const desc = opt.description ? theme.fg(\"muted\", opt.description) : \"\";\n addWrappedWithPrefix(\" \", desc + theme.bold(`(推荐:${detail})`));\n } else if (opt.description) {\n addWrappedWithPrefix(\" \", theme.fg(\"muted\", opt.description));\n }\n }\n }\n\n const q = currentQuestion();\n\n // Content\n if (inputMode && q) {\n renderPromptAndRecommendation(q);\n if (q.type === \"choice\") renderOptions();\n lines.push(\"\");\n addWrappedWithPrefix(\" \", theme.fg(\"muted\", \"Your answer:\"));\n for (const line of editor.render(Math.max(1, renderWidth - 2))) {\n lines.push(` ${line}`);\n }\n lines.push(\"\");\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", \"Enter to submit • Esc to cancel\"));\n } else if (currentTab === submitTab) {\n addWrappedWithPrefix(\" \", theme.fg(\"accent\", theme.bold(\"Ready to submit\")));\n lines.push(\"\");\n for (const question of questions) {\n const answer = answers.get(question.id);\n if (answer) {\n const prefix = answer.wasCustom ? \"(wrote) \" : \"\";\n const summary = `${theme.fg(\"muted\", `${questionLabel(question)}: `)}${theme.fg(\"text\", prefix + answer.label)}`;\n addWrappedWithPrefix(\" \", summary);\n }\n }\n lines.push(\"\");\n if (allAnswered()) {\n addWrappedWithPrefix(\" \", theme.fg(\"success\", \"Press Enter to submit\"));\n } else {\n const missing = questions\n .filter((x) => !answers.has(x.id))\n .map((x) => questionLabel(x))\n .join(\", \");\n addWrappedWithPrefix(\" \", theme.fg(\"warning\", `Unanswered: ${missing}`));\n }\n } else if (q) {\n renderPromptAndRecommendation(q);\n if (q.type === \"choice\") {\n renderOptions();\n } else {\n const answer = answers.get(q.id);\n addWrappedWithPrefix(\" \", theme.fg(\"muted\", \"Free-form answer\"));\n lines.push(\"\");\n if (answer) {\n addWrappedWithPrefix(\" \", theme.fg(\"text\", ` ${answer.label}`));\n lines.push(\"\");\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", \"Enter to edit • Esc cancel\"));\n } else {\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", \"Press Enter to type your answer\"));\n }\n }\n }\n\n lines.push(\"\");\n if (!inputMode) {\n const help = isMulti\n ? \"Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel\"\n : \"↑↓ select • Enter confirm • Esc cancel\";\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", help));\n }\n lines.push(theme.fg(\"accent\", \"─\".repeat(renderWidth)));\n\n cachedLines = lines;\n return lines;\n }\n\n return {\n render,\n invalidate: () => {\n cachedLines = undefined;\n },\n handleInput,\n };\n });\n}\n\n// ---------- RPC fallback (per-question dialogs) ----------\n\nasync function presentRpcDialogs(\n ctx: ExtensionContext,\n questions: Question[],\n): Promise<AskUserResult> {\n const answers: Answer[] = [];\n let cancelled = false;\n\n for (const q of questions) {\n if (q.type === \"choice\") {\n // Merge the recommendation detail into the recommended option's label\n // (e.g. \"保留 fallback 行(推荐:…)\"), then reverse-map the selected\n // display string back to the original option index.\n const opts = q.options ?? [];\n const recIdx = recommendedIndex(q);\n const display: { index: number; label: string }[] = opts.map((o, idx) => ({\n index: idx,\n label:\n idx === recIdx && q.recommendation\n ? `${o.label}(推荐:${stripRecommendationTitle(q.recommendation, o)})`\n : o.label,\n }));\n if (q.allowOther) display.push({ index: -1, label: \"Type something...\" });\n const label = questionLabel(q);\n const title = label ? `${label}: ${q.prompt}` : q.prompt;\n const choice = await ctx.ui.select(title, display.map((d) => d.label));\n if (choice === undefined) {\n cancelled = true;\n break;\n }\n if (choice === \"Type something...\") {\n const value = await ctx.ui.input(q.prompt, \"Type your answer\");\n if (value === undefined) {\n cancelled = true;\n break;\n }\n answers.push({ id: q.id, value, label: value, wasCustom: true });\n } else {\n const found = display.find((d) => d.label === choice);\n const idx = found ? found.index : -1;\n const opt = idx >= 0 ? opts[idx] : undefined;\n answers.push({\n id: q.id,\n value: opt?.value ?? choice,\n label: opt?.label ?? choice,\n wasCustom: false,\n index: idx >= 0 ? idx + 1 : undefined,\n });\n }\n } else {\n const value = await ctx.ui.input(q.prompt, questionLabel(q) || q.prompt);\n if (value === undefined) {\n cancelled = true;\n break;\n }\n answers.push({ id: q.id, value, label: value, wasCustom: true });\n }\n }\n\n return { questions, answers, cancelled };\n}\n\n// ---------- Extension ----------\n\nexport default function askUserExtension(pi: ExtensionAPI): void {\n pi.registerTool({\n name: \"ask_user\",\n label: \"Ask User\",\n description:\n \"Ask the user one or more questions as an interactive form (choice options or free-text). \" +\n \"Use when you need the user's decision, preference, or input to continue — especially to \" +\n \"present a round of design/planning questions with your recommended answer for each. \" +\n \"Each question may include a 'recommendation'; when it matches one of the options, that option is highlighted.\",\n promptSnippet: \"Ask the user questions through an interactive form\",\n promptGuidelines: [\n \"Use ask_user to put questions to the user as an interactive form instead of printing plain-text Q1..QN blocks.\",\n \"When a single turn has multiple related questions (e.g. a grilling round's frontier), pass them all in one ask_user call — one question per entry, with type 'choice' or 'text'.\",\n \"For each question you can include a 'recommendation' with your recommended answer. When it matches an option's value or label, that option is highlighted in the form; otherwise it is shown as a hint under the question.\",\n \"For grilling-style rounds set numbered: true so the questions are prefixed Q1/Q2. For ordinary questions omit it — the form then shows just the prompt (plus an optional title).\",\n \"ask_user works in TUI mode (full form) and RPC mode (sequential dialogs). In print/json mode it returns the questions as text so you can ask them in plain text.\",\n \"If ask_user reports 'cancelled', stop and let the user redirect instead of re-asking the same questions.\",\n ],\n parameters: AskUserParams,\n executionMode: \"sequential\",\n\n async execute(_toolCallId, params, signal, _onUpdate, ctx) {\n if (signal?.aborted) {\n return textResult(\"Cancelled\", [], true);\n }\n const questions = defaultQuestions(params);\n\n if (ctx.mode === \"tui\") {\n const result = await presentTuiForm(ctx, questions);\n if (result.cancelled) {\n return {\n content: [{ type: \"text\", text: \"User cancelled the questions\" }],\n details: result,\n };\n }\n return {\n content: [{ type: \"text\", text: formatAnswers(questions, result.answers) }],\n details: result,\n };\n }\n\n if (ctx.mode === \"rpc\") {\n const result = await presentRpcDialogs(ctx, questions);\n if (result.cancelled) {\n return {\n content: [{ type: \"text\", text: \"User cancelled the questions\" }],\n details: result,\n };\n }\n return {\n content: [{ type: \"text\", text: formatAnswers(questions, result.answers) }],\n details: result,\n };\n }\n\n // Non-interactive modes: structured fallback so the LLM asks in plain text.\n const fallback = questions\n .map((q) => {\n const rec = q.recommendation ? `\\n Recommended: ${q.recommendation}` : \"\";\n const opts =\n q.type === \"choice\"\n ? `\\n Options: ${(q.options ?? []).map((o) => `${o.label}`).join(\" | \")}`\n : \"\";\n const label = questionLabel(q);\n return `${label ? `${label}: ` : \"\"}${q.prompt}${opts}${rec}`;\n })\n .join(\"\\n\\n\");\n return {\n content: [\n {\n type: \"text\",\n text:\n \"Interactive form unavailable in this mode. Ask the user the following questions as plain text (numbered Q1..QN):\\n\\n\" +\n fallback,\n },\n ],\n details: { questions, answers: [], cancelled: false },\n };\n },\n\n renderCall(args, theme, context) {\n const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n const qs = Array.isArray(args.questions) ? (args.questions as Question[]) : [];\n const labels = qs\n .map((q, i) => (q.title ? `Q${i + 1} - ${q.title}` : q.id || `Q${i + 1}`))\n .join(\", \");\n let content = theme.fg(\"toolTitle\", theme.bold(\"ask_user \"));\n content += theme.fg(\"muted\", `${qs.length} question${qs.length !== 1 ? \"s\" : \"\"}`);\n if (labels) content += theme.fg(\"dim\", ` (${labels})`);\n text.setText(content);\n return text;\n },\n\n renderResult(result, _options, theme, context) {\n const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n const details = result.details as AskUserResult | undefined;\n if (!details) {\n const out = result.content\n .filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n text.setText(theme.fg(\"warning\", out || \"ask_user\"));\n return text;\n }\n if (details.cancelled) {\n text.setText(theme.fg(\"warning\", \"Cancelled\"));\n return text;\n }\n const lines = details.answers.map((a) => {\n const q = details.questions.find((x) => x.id === a.id);\n const label = q ? questionLabel(q) || `Q${q.number}` : a.id;\n if (a.wasCustom) {\n return `${theme.fg(\"success\", \"✓ \")}${theme.fg(\"accent\", label)}: ${theme.fg(\"muted\", \"(wrote) \")}${a.label}`;\n }\n const display = a.index ? `${a.index}. ${a.label}` : a.label;\n return `${theme.fg(\"success\", \"✓ \")}${theme.fg(\"accent\", label)}: ${display}`;\n });\n text.setText(lines.join(\"\\n\"));\n return text;\n },\n });\n}\n"
|
|
5
|
+
"/**\n * Ask User Questions - interactive question form tool\n *\n * Lets the LLM ask the user one or more questions as an interactive form\n * (choice + free-text), instead of dumping plain-text \"Q1..QN\" blocks.\n *\n * Mode behavior:\n * - TUI mode: full-screen tabbed form via ctx.ui.custom()\n * - RPC mode: per-question select/input dialogs over the extension UI protocol\n * - print/json mode: structured fallback so the LLM can ask in plain text\n *\n * Each question carries a \"recommendation\" hint (the grilling skill's\n * \"➡️ recommended answer\"). When it matches one of a choice question's\n * options, it is highlighted on that option; otherwise it is shown dimmed\n * under the prompt.\n */\n\nimport type { ExtensionAPI, ExtensionContext, Theme } from \"@earendil-works/pi-coding-agent\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport {\n Editor,\n type EditorTheme,\n Key,\n matchesKey,\n Text,\n visibleWidth,\n wrapTextWithAnsi,\n} from \"@earendil-works/pi-tui\";\nimport { Type } from \"typebox\";\nimport { assistantText, classifyTurn } from \"./diagnostic.ts\";\n\n// ---------- Types ----------\n\ninterface QuestionOption {\n value: string;\n label: string;\n description?: string;\n}\n\ninterface Question {\n id: string;\n title?: string;\n prompt: string;\n type: \"choice\" | \"text\";\n options?: QuestionOption[];\n allowOther: boolean;\n recommendation?: string;\n number: number;\n numbered: boolean;\n}\n\ninterface Answer {\n id: string;\n value: string;\n label: string;\n wasCustom: boolean;\n index?: number;\n}\n\ninterface AskUserResult {\n questions: Question[];\n answers: Answer[];\n cancelled: boolean;\n}\n\n// ---------- Schema ----------\n\nconst QuestionOptionSchema = Type.Object({\n value: Type.String({ description: \"Value returned to the LLM when this option is selected\" }),\n label: Type.String({ description: \"Display label shown to the user\" }),\n description: Type.Optional(\n Type.String({ description: \"Optional description shown under the label\" }),\n ),\n});\n\nconst QuestionSchema = Type.Object({\n id: Type.String({ description: \"Unique question id (e.g. q1, q2, scope)\" }),\n title: Type.Optional(\n Type.String({ description: \"Optional short title shown after the question number, e.g. 'Scope'\" }),\n ),\n prompt: Type.String({ description: \"Full question text to display\" }),\n type: StringEnum([\"choice\", \"text\"] as const, {\n description: \"'choice' for an options list, 'text' for free-form input\",\n }),\n options: Type.Optional(\n Type.Array(QuestionOptionSchema, { description: \"Options for type=choice (required for choice)\" }),\n ),\n allowOther: Type.Optional(\n Type.Boolean({ description: \"Choice questions: allow free-text 'Type something' (default true)\" }),\n ),\n recommendation: Type.Optional(\n Type.String({\n description:\n \"Your recommended answer (the grilling '➡️ recommended answer'). When it matches an option's value or label, that option is highlighted in the form.\",\n }),\n ),\n});\n\nconst AskUserParams = Type.Object({\n questions: Type.Array(QuestionSchema, {\n description: \"One or more questions to ask the user in a single interactive form\",\n minItems: 1,\n maxItems: 10,\n }),\n numbered: Type.Optional(\n Type.Boolean({\n description: \"Prefix each question with its number (Q1, Q2…) in grilling style. Default: false.\",\n }),\n ),\n});\n\n// ---------- Helpers ----------\n\n/** Raw question shape as accepted by the tool (optional fields not yet normalized). */\ninterface RawQuestion {\n id: string;\n title?: string;\n prompt: string;\n type: \"choice\" | \"text\";\n options?: QuestionOption[];\n allowOther?: boolean;\n recommendation?: string;\n}\n\nfunction defaultQuestions(params: {\n questions: RawQuestion[];\n numbered?: boolean;\n}): Question[] {\n return params.questions.map((q, i) => ({\n ...q,\n allowOther: q.allowOther !== false,\n options: q.type === \"choice\" ? q.options ?? [] : undefined,\n number: i + 1,\n numbered: params.numbered === true,\n }));\n}\n\n/**\n * Question label: \"Q1 - Scope\" when numbered, \"Scope\" when not. Empty string\n * when not numbered and no title is set.\n */\nfunction questionLabel(q: Pick<Question, \"number\" | \"numbered\" | \"title\">): string {\n if (q.numbered) return `Q${q.number}${q.title ? ` - ${q.title}` : \"\"}`;\n return q.title ?? \"\";\n}\n\n/** Whether a question's recommendation points at a specific option (heuristic match). */\nfunction recommendationMatch(q: Question, opt: QuestionOption): boolean {\n const rec = q.recommendation?.trim().toLowerCase();\n if (!rec) return false;\n const value = (opt.value ?? \"\").trim().toLowerCase();\n const label = (opt.label ?? \"\").trim().toLowerCase();\n if (value && rec === value) return true;\n if (label && rec === label) return true;\n if (value.length >= 3 && rec.includes(value)) return true;\n if (label.length >= 3 && rec.includes(label)) return true;\n if (rec.length >= 3) {\n if (value && value.includes(rec)) return true;\n if (label && label.includes(rec)) return true;\n }\n return false;\n}\n\n/**\n * Index of the option a question's recommendation points at, or -1 when the\n * recommendation is absent or matches no option. Returns the first match.\n */\nfunction recommendedIndex(q: Question): number {\n if (!q.recommendation || q.type !== \"choice\") return -1;\n const opts = q.options ?? [];\n for (let i = 0; i < opts.length; i++) {\n if (recommendationMatch(q, opts[i])) return i;\n }\n return -1;\n}\n\n/**\n * Reduce a recommendation to its \"detail\" for display.\n * Grill recommendation shape is \"<标题> - <详情>\"; we take only the <详情>\n * after the first dash separator (em/en dash, spaced hyphen, fullwidth\n * hyphen-minus), so a leading echo of the option title is dropped even when\n * the title carries extra text like \"(现状)\". A leading echo of the option\n * label/value is also stripped for the no-dash case.\n */\nfunction stripRecommendationTitle(rec: string, opt?: QuestionOption): string {\n let cleaned = rec.trim();\n if (opt) {\n for (const token of [opt.label, opt.value]) {\n const t = token?.trim();\n if (!t) continue;\n if (cleaned.toLowerCase().startsWith(t.toLowerCase())) {\n cleaned = cleaned.slice(t.length).trimStart();\n break;\n }\n }\n }\n const m = cleaned.match(/[\\u2014\\u2013]| - |-/);\n if (m && typeof m.index === \"number\") {\n cleaned = cleaned\n .slice(m.index + m[0].length)\n .replace(/^[\\s::,,、.…-]+/, \"\");\n }\n return cleaned.trim() || rec.trim();\n}\n\nfunction textResult(\n message: string,\n questions: Question[],\n cancelled = true,\n): { content: { type: \"text\"; text: string }[]; details: AskUserResult } {\n return {\n content: [{ type: \"text\", text: message }],\n details: { questions, answers: [], cancelled },\n };\n}\n\nfunction formatAnswers(questions: Question[], answers: Answer[]): string {\n const lines = answers.map((a) => {\n const q = questions.find((x) => x.id === a.id);\n const label = q ? questionLabel(q) || `Q${q.number}` : a.id;\n if (a.wasCustom) return `${label}: user wrote: ${a.label}`;\n const idx = a.index ? ` ${a.index}.` : \"\";\n return `${label}: user selected:${idx} ${a.label}`;\n });\n return lines.join(\"\\n\");\n}\n\n// ---------- TUI form ----------\n\nfunction editorTheme(theme: Theme): EditorTheme {\n return {\n borderColor: (s) => theme.fg(\"accent\", s),\n selectList: {\n selectedPrefix: (t) => theme.fg(\"accent\", t),\n selectedText: (t) => theme.fg(\"accent\", t),\n description: (t) => theme.fg(\"muted\", t),\n scrollInfo: (t) => theme.fg(\"dim\", t),\n noMatch: (t) => theme.fg(\"warning\", t),\n },\n };\n}\n\nasync function presentTuiForm(\n ctx: ExtensionContext,\n questions: Question[],\n): Promise<AskUserResult> {\n return ctx.ui.custom<AskUserResult>((tui, theme, _kb, done) => {\n const isMulti = questions.length > 1;\n const submitTab = questions.length;\n\n let currentTab = 0;\n let optionIndex = 0;\n let inputMode = false;\n let inputQuestionId: string | null = null;\n let cachedLines: string[] | undefined;\n const answers = new Map<string, Answer>();\n\n const editor = new Editor(tui, editorTheme(theme));\n\n // ---------- helpers ----------\n\n function refresh() {\n cachedLines = undefined;\n tui.requestRender();\n }\n\n function submit(cancelled: boolean) {\n done({ questions, answers: Array.from(answers.values()), cancelled });\n }\n\n function currentQuestion(): Question | undefined {\n return questions[currentTab];\n }\n\n function currentOptions(): Array<QuestionOption & { isOther?: boolean }> {\n const q = currentQuestion();\n if (!q || q.type !== \"choice\") return [];\n const opts: Array<QuestionOption & { isOther?: boolean }> = [...(q.options ?? [])];\n if (q.allowOther) {\n opts.push({ value: \"__other__\", label: \"Type something.\", isOther: true });\n }\n return opts;\n }\n\n function allAnswered(): boolean {\n return questions.every((q) => answers.has(q.id));\n }\n\n function advanceAfterAnswer() {\n if (!isMulti) {\n submit(false);\n return;\n }\n if (currentTab < submitTab - 1) {\n currentTab++;\n } else {\n currentTab = submitTab;\n }\n optionIndex = 0;\n inputMode = false;\n inputQuestionId = null;\n refresh();\n }\n\n function saveAnswer(\n questionId: string,\n value: string,\n label: string,\n wasCustom: boolean,\n index?: number,\n ) {\n answers.set(questionId, { id: questionId, value, label, wasCustom, index });\n }\n\n function openInput(questionId: string) {\n inputMode = true;\n inputQuestionId = questionId;\n editor.setText(\"\");\n refresh();\n }\n\n editor.onSubmit = (value) => {\n if (!inputQuestionId) return;\n const trimmed = value.trim() || \"(no response)\";\n saveAnswer(inputQuestionId, trimmed, trimmed, true);\n inputMode = false;\n inputQuestionId = null;\n editor.setText(\"\");\n advanceAfterAnswer();\n };\n\n // ---------- input ----------\n\n function handleInput(data: string) {\n if (inputMode) {\n if (matchesKey(data, Key.escape)) {\n inputMode = false;\n inputQuestionId = null;\n editor.setText(\"\");\n refresh();\n return;\n }\n editor.handleInput(data);\n refresh();\n return;\n }\n\n // Tab navigation (multi-question only)\n if (isMulti) {\n if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {\n currentTab = (currentTab + 1) % (submitTab + 1);\n optionIndex = 0;\n refresh();\n return;\n }\n if (matchesKey(data, Key.shift(\"tab\")) || matchesKey(data, Key.left)) {\n currentTab = (currentTab - 1 + submitTab + 1) % (submitTab + 1);\n optionIndex = 0;\n refresh();\n return;\n }\n }\n\n const q = currentQuestion();\n\n // Submit tab\n if (currentTab === submitTab) {\n if (matchesKey(data, Key.enter) && allAnswered()) {\n submit(false);\n } else if (matchesKey(data, Key.escape)) {\n submit(true);\n }\n return;\n }\n\n if (!q) return;\n\n if (q.type === \"text\") {\n if (matchesKey(data, Key.enter)) {\n openInput(q.id);\n } else if (matchesKey(data, Key.escape)) {\n submit(true);\n }\n return;\n }\n\n // Choice navigation\n const opts = currentOptions();\n if (matchesKey(data, Key.up)) {\n optionIndex = Math.max(0, optionIndex - 1);\n refresh();\n return;\n }\n if (matchesKey(data, Key.down)) {\n optionIndex = Math.min(opts.length - 1, optionIndex + 1);\n refresh();\n return;\n }\n if (matchesKey(data, Key.enter)) {\n const opt = opts[optionIndex];\n if (!opt) return;\n if (opt.isOther) {\n openInput(q.id);\n return;\n }\n saveAnswer(q.id, opt.value, opt.label, false, optionIndex + 1);\n advanceAfterAnswer();\n return;\n }\n if (matchesKey(data, Key.escape)) {\n submit(true);\n }\n }\n\n // ---------- render ----------\n\n function render(width: number): string[] {\n if (cachedLines) return cachedLines;\n\n const lines: string[] = [];\n const renderWidth = Math.max(1, width);\n\n function addWrapped(text: string) {\n lines.push(...wrapTextWithAnsi(text, renderWidth));\n }\n\n function addWrappedWithPrefix(prefix: string, text: string) {\n const prefixWidth = visibleWidth(prefix);\n if (prefixWidth >= renderWidth) {\n addWrapped(prefix + text);\n return;\n }\n const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);\n const continuationPrefix = \" \".repeat(prefixWidth);\n for (let i = 0; i < wrapped.length; i++) {\n lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`);\n }\n }\n\n function renderPromptAndRecommendation(q: Question) {\n const recOnOption = recommendedIndex(q) >= 0;\n const label = questionLabel(q);\n const head = label\n ? `${theme.fg(\"accent\", theme.bold(`${label}:`))} ${theme.fg(\"text\", q.prompt)}`\n : theme.fg(\"text\", q.prompt);\n addWrappedWithPrefix(\" \", head);\n if (q.recommendation && !recOnOption) {\n lines.push(\"\");\n addWrappedWithPrefix(\n \" \",\n theme.fg(\"dim\", `Recommended: ${stripRecommendationTitle(q.recommendation)}`),\n );\n }\n lines.push(\"\");\n }\n\n lines.push(theme.fg(\"accent\", \"─\".repeat(renderWidth)));\n\n // Tab bar (multi-question only)\n if (isMulti) {\n const tabs: string[] = [\"← \"];\n for (let i = 0; i < questions.length; i++) {\n const isActive = i === currentTab;\n const isAnswered = answers.has(questions[i].id);\n const lbl = questions[i].title ?? `Q${i + 1}`;\n const box = isAnswered ? \"■\" : \"□\";\n const color = isAnswered ? \"success\" : \"muted\";\n const text = ` ${box} ${lbl} `;\n const styled = isActive\n ? theme.bg(\"selectedBg\", theme.fg(\"text\", text))\n : theme.fg(color, text);\n tabs.push(`${styled} `);\n }\n const canSubmit = allAnswered();\n const isSubmitTab = currentTab === submitTab;\n const submitText = \" ✓ Submit \";\n const submitStyled = isSubmitTab\n ? theme.bg(\"selectedBg\", theme.fg(\"text\", submitText))\n : theme.fg(canSubmit ? \"success\" : \"dim\", submitText);\n tabs.push(`${submitStyled} →`);\n addWrappedWithPrefix(\" \", tabs.join(\"\"));\n lines.push(\"\");\n }\n\n // Render an option list, highlighting the recommended option if any.\n function renderOptions() {\n const opts = currentOptions();\n const q = currentQuestion();\n const recIdx = q ? recommendedIndex(q) : -1;\n for (let i = 0; i < opts.length; i++) {\n const opt = opts[i];\n const selected = i === optionIndex;\n const isOther = opt.isOther === true;\n const recommended = !isOther && i === recIdx;\n const prefix = selected ? theme.fg(\"accent\", \"> \") : \" \";\n const labelBase = `${i + 1}. ${recommended ? \"★ \" : \"\"}${opt.label}${isOther && inputMode ? \" ✎\" : \"\"}`;\n const label = recommended ? theme.bold(labelBase) : labelBase;\n const color = selected || (isOther && inputMode) ? \"accent\" : \"text\";\n addWrappedWithPrefix(prefix, theme.fg(color, label));\n if (recommended && q?.recommendation) {\n // Recommended option: description (muted) then the recommendation\n // detail (default + bold) wrapped as (推荐:<详情>), same line.\n const detail = stripRecommendationTitle(q.recommendation, opt);\n const desc = opt.description ? theme.fg(\"muted\", opt.description) : \"\";\n addWrappedWithPrefix(\" \", desc + theme.bold(`(推荐:${detail})`));\n } else if (opt.description) {\n addWrappedWithPrefix(\" \", theme.fg(\"muted\", opt.description));\n }\n }\n }\n\n const q = currentQuestion();\n\n // Content\n if (inputMode && q) {\n renderPromptAndRecommendation(q);\n if (q.type === \"choice\") renderOptions();\n lines.push(\"\");\n addWrappedWithPrefix(\" \", theme.fg(\"muted\", \"Your answer:\"));\n for (const line of editor.render(Math.max(1, renderWidth - 2))) {\n lines.push(` ${line}`);\n }\n lines.push(\"\");\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", \"Enter to submit • Esc to cancel\"));\n } else if (currentTab === submitTab) {\n addWrappedWithPrefix(\" \", theme.fg(\"accent\", theme.bold(\"Ready to submit\")));\n lines.push(\"\");\n for (const question of questions) {\n const answer = answers.get(question.id);\n if (answer) {\n const prefix = answer.wasCustom ? \"(wrote) \" : \"\";\n const summary = `${theme.fg(\"muted\", `${questionLabel(question)}: `)}${theme.fg(\"text\", prefix + answer.label)}`;\n addWrappedWithPrefix(\" \", summary);\n }\n }\n lines.push(\"\");\n if (allAnswered()) {\n addWrappedWithPrefix(\" \", theme.fg(\"success\", \"Press Enter to submit\"));\n } else {\n const missing = questions\n .filter((x) => !answers.has(x.id))\n .map((x) => questionLabel(x))\n .join(\", \");\n addWrappedWithPrefix(\" \", theme.fg(\"warning\", `Unanswered: ${missing}`));\n }\n } else if (q) {\n renderPromptAndRecommendation(q);\n if (q.type === \"choice\") {\n renderOptions();\n } else {\n const answer = answers.get(q.id);\n addWrappedWithPrefix(\" \", theme.fg(\"muted\", \"Free-form answer\"));\n lines.push(\"\");\n if (answer) {\n addWrappedWithPrefix(\" \", theme.fg(\"text\", ` ${answer.label}`));\n lines.push(\"\");\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", \"Enter to edit • Esc cancel\"));\n } else {\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", \"Press Enter to type your answer\"));\n }\n }\n }\n\n lines.push(\"\");\n if (!inputMode) {\n const help = isMulti\n ? \"Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel\"\n : \"↑↓ select • Enter confirm • Esc cancel\";\n addWrappedWithPrefix(\" \", theme.fg(\"dim\", help));\n }\n lines.push(theme.fg(\"accent\", \"─\".repeat(renderWidth)));\n\n cachedLines = lines;\n return lines;\n }\n\n return {\n render,\n invalidate: () => {\n cachedLines = undefined;\n },\n handleInput,\n };\n });\n}\n\n// ---------- RPC fallback (per-question dialogs) ----------\n\nasync function presentRpcDialogs(\n ctx: ExtensionContext,\n questions: Question[],\n): Promise<AskUserResult> {\n const answers: Answer[] = [];\n let cancelled = false;\n\n for (const q of questions) {\n if (q.type === \"choice\") {\n // Merge the recommendation detail into the recommended option's label\n // (e.g. \"保留 fallback 行(推荐:…)\"), then reverse-map the selected\n // display string back to the original option index.\n const opts = q.options ?? [];\n const recIdx = recommendedIndex(q);\n const display: { index: number; label: string }[] = opts.map((o, idx) => ({\n index: idx,\n label:\n idx === recIdx && q.recommendation\n ? `${o.label}(推荐:${stripRecommendationTitle(q.recommendation, o)})`\n : o.label,\n }));\n if (q.allowOther) display.push({ index: -1, label: \"Type something...\" });\n const label = questionLabel(q);\n const title = label ? `${label}: ${q.prompt}` : q.prompt;\n const choice = await ctx.ui.select(title, display.map((d) => d.label));\n if (choice === undefined) {\n cancelled = true;\n break;\n }\n if (choice === \"Type something...\") {\n const value = await ctx.ui.input(q.prompt, \"Type your answer\");\n if (value === undefined) {\n cancelled = true;\n break;\n }\n answers.push({ id: q.id, value, label: value, wasCustom: true });\n } else {\n const found = display.find((d) => d.label === choice);\n const idx = found ? found.index : -1;\n const opt = idx >= 0 ? opts[idx] : undefined;\n answers.push({\n id: q.id,\n value: opt?.value ?? choice,\n label: opt?.label ?? choice,\n wasCustom: false,\n index: idx >= 0 ? idx + 1 : undefined,\n });\n }\n } else {\n const value = await ctx.ui.input(q.prompt, questionLabel(q) || q.prompt);\n if (value === undefined) {\n cancelled = true;\n break;\n }\n answers.push({ id: q.id, value, label: value, wasCustom: true });\n }\n }\n\n return { questions, answers, cancelled };\n}\n\n// ---------- Extension ----------\n\nexport default function askUserExtension(pi: ExtensionAPI): void {\n pi.registerTool({\n name: \"ask_user\",\n label: \"Ask User\",\n description:\n \"Ask the user one or more questions as an interactive form (choice options or free-text). \" +\n \"Use it whenever you need the user's decision, preference, or input to continue. \" +\n \"In grilling, domain-modeling, or design-interview workflows that present question rounds \" +\n \"as numbered plain-text blocks (Q1..QN, ❓, 'recommended answer'), you MUST call ask_user \" +\n \"to present those questions as a form instead of typing them out. \" +\n \"Each question may include a 'recommendation'; when it matches one of the options, that option is highlighted.\",\n promptSnippet:\n \"Present user questions as an interactive form (mandatory for grilling/design question rounds)\",\n promptGuidelines: [\n \"In grilling, domain-modeling, or any design-interview workflow (e.g. the grilling / grill-with-docs / domain-modeling skills) that presents question rounds as numbered plain-text blocks (Q1..QN, ❓, 'recommended answer'), you MUST call ask_user to present each round as an interactive form — do not write the questions out as plain text.\",\n \"Use ask_user whenever you need the user's decision, preference, or free-text input to continue a task, instead of typing the questions inline.\",\n \"When a single turn has multiple related questions (e.g. a grilling round's frontier), pass them all in one ask_user call — one question per entry, with type 'choice' or 'text'.\",\n \"For each ask_user question you can include a 'recommendation' with your recommended answer. When it matches one of the options, that option is highlighted; otherwise it is shown as a hint under the question.\",\n \"For grilling-style rounds set numbered: true so the questions are prefixed Q1/Q2. For ordinary questions omit it — the form then shows just the prompt (plus an optional title).\",\n \"ask_user works in TUI mode (full form) and RPC mode (sequential dialogs). In print/json mode it returns the questions as text, so reserve it for interactive sessions where a form can render.\",\n \"If ask_user reports 'cancelled', stop and let the user redirect instead of re-asking the same questions.\",\n ],\n parameters: AskUserParams,\n executionMode: \"sequential\",\n\n async execute(_toolCallId, params, signal, _onUpdate, ctx) {\n if (signal?.aborted) {\n return textResult(\"Cancelled\", [], true);\n }\n const questions = defaultQuestions(params);\n\n if (ctx.mode === \"tui\") {\n const result = await presentTuiForm(ctx, questions);\n if (result.cancelled) {\n return {\n content: [{ type: \"text\", text: \"User cancelled the questions\" }],\n details: result,\n };\n }\n return {\n content: [{ type: \"text\", text: formatAnswers(questions, result.answers) }],\n details: result,\n };\n }\n\n if (ctx.mode === \"rpc\") {\n const result = await presentRpcDialogs(ctx, questions);\n if (result.cancelled) {\n return {\n content: [{ type: \"text\", text: \"User cancelled the questions\" }],\n details: result,\n };\n }\n return {\n content: [{ type: \"text\", text: formatAnswers(questions, result.answers) }],\n details: result,\n };\n }\n\n // Non-interactive modes: structured fallback so the LLM asks in plain text.\n const fallback = questions\n .map((q) => {\n const rec = q.recommendation ? `\\n Recommended: ${q.recommendation}` : \"\";\n const opts =\n q.type === \"choice\"\n ? `\\n Options: ${(q.options ?? []).map((o) => `${o.label}`).join(\" | \")}`\n : \"\";\n const label = questionLabel(q);\n return `${label ? `${label}: ` : \"\"}${q.prompt}${opts}${rec}`;\n })\n .join(\"\\n\\n\");\n return {\n content: [\n {\n type: \"text\",\n text:\n \"Interactive form unavailable in this mode. Ask the user the following questions as plain text (numbered Q1..QN):\\n\\n\" +\n fallback,\n },\n ],\n details: { questions, answers: [], cancelled: false },\n };\n },\n\n renderCall(args, theme, context) {\n const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n const qs = Array.isArray(args.questions) ? (args.questions as Question[]) : [];\n const labels = qs\n .map((q, i) => (q.title ? `Q${i + 1} - ${q.title}` : q.id || `Q${i + 1}`))\n .join(\", \");\n let content = theme.fg(\"toolTitle\", theme.bold(\"ask_user \"));\n content += theme.fg(\"muted\", `${qs.length} question${qs.length !== 1 ? \"s\" : \"\"}`);\n if (labels) content += theme.fg(\"dim\", ` (${labels})`);\n text.setText(content);\n return text;\n },\n\n renderResult(result, _options, theme, context) {\n const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n const details = result.details as AskUserResult | undefined;\n if (!details) {\n const out = result.content\n .filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n text.setText(theme.fg(\"warning\", out || \"ask_user\"));\n return text;\n }\n if (details.cancelled) {\n text.setText(theme.fg(\"warning\", \"Cancelled\"));\n return text;\n }\n const lines = details.answers.map((a) => {\n const q = details.questions.find((x) => x.id === a.id);\n const label = q ? questionLabel(q) || `Q${q.number}` : a.id;\n if (a.wasCustom) {\n return `${theme.fg(\"success\", \"✓ \")}${theme.fg(\"accent\", label)}: ${theme.fg(\"muted\", \"(wrote) \")}${a.label}`;\n }\n const display = a.index ? `${a.index}. ${a.label}` : a.label;\n return `${theme.fg(\"success\", \"✓ \")}${theme.fg(\"accent\", label)}: ${display}`;\n });\n text.setText(lines.join(\"\\n\"));\n return text;\n },\n });\n\n // ---- Lightweight per-turn diagnostic (ADR-0001 escalation gate) ----\n // With --ask-user-debug, records each turn whether a \"question round\" happened\n // and whether it used ask_user, so the escalation gate has evidence on whether\n // pure tool metadata is enough to make grilling/design rounds use the form.\n let usedAskUserThisTurn = false;\n\n pi.registerFlag(\"ask-user-debug\", {\n description: \"Log per-turn question-round diagnostics for ask_user triggering\",\n type: \"boolean\",\n default: false,\n });\n\n const askUserDebug = (): boolean => pi.getFlag(\"ask-user-debug\") === true;\n\n pi.on(\"turn_start\", () => {\n if (askUserDebug()) usedAskUserThisTurn = false;\n });\n\n pi.on(\"tool_call\", (event, _ctx) => {\n if (askUserDebug() && event.toolName === \"ask_user\") usedAskUserThisTurn = true;\n });\n\n pi.on(\"turn_end\", (event, _ctx) => {\n if (!askUserDebug()) return;\n const text = assistantText(event.message);\n const diag = classifyTurn(text, usedAskUserThisTurn);\n if (!diag.isQuestionRound) return;\n const via = diag.usedAskUser ? \"ask_user\" : \"plain text\";\n console.log(\n `[pi-ask-user] turn ${event.turnIndex}: question round via ${via}` +\n ` (wrotePlain=${diag.wrotePlainQuestions}, clean=${diag.handledCleanly})`,\n );\n });\n}\n",
|
|
6
|
+
"/**\n * Lightweight per-turn diagnostic for the ask_user triggering workstream.\n *\n * Supports the escalation gate (ADR-0001): classifies each assistant turn as a\n * \"question round\" and records whether it used ask_user, so we can judge\n * whether pure tool metadata is enough to make grilling/design-interview\n * rounds use the form, or whether we need to escalate to per-turn injection.\n *\n * Pure functions only — no pi imports — so it can be unit-tested directly.\n */\n\nexport interface TurnDiagnostic {\n /** Whether this turn was a question round (asked via ask_user or plain text). */\n isQuestionRound: boolean;\n /** Whether the turn called the ask_user tool. */\n usedAskUser: boolean;\n /** Whether the assistant wrote plain-text question markers (Q1..QN, ❓, …). */\n wrotePlainQuestions: boolean;\n /**\n * True when a question round was handled via ask_user without plain-text\n * question leakage. The ideal outcome for the metadata-first approach.\n */\n handledCleanly: boolean;\n}\n\n/** Heuristic markers for \"this assistant text is a question round\" (tunable). */\nconst QUESTION_MARKERS: RegExp[] = [\n /\\bQ\\s?\\d+\\b/i, // Q1, Q2, Q 3 — numbered question rounds\n /❓/u, // grilling question marker\n /recommended answer|推荐答案/i,\n /请回答|请选择|你决定|你的选择|你更倾向|选一个/i,\n];\n\nexport function isQuestionRoundText(text: string): boolean {\n if (!text) return false;\n return QUESTION_MARKERS.some((re) => re.test(text));\n}\n\n/** Extract the assistant's answer text, skipping thinking content blocks. */\nexport function assistantText(message: unknown): string {\n if (!message || typeof message !== \"object\") return \"\";\n const content = (message as { content?: unknown }).content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .filter(\n (block): block is { type: string; text: string } =>\n typeof block === \"object\" &&\n block !== null &&\n (block as { type?: unknown }).type === \"text\",\n )\n .map((block) => (block as { text?: unknown }).text ?? \"\")\n .join(\"\\n\");\n}\n\nexport function classifyTurn(assistantTextValue: string, usedAskUser: boolean): TurnDiagnostic {\n const wrotePlainQuestions = isQuestionRoundText(assistantTextValue);\n const isQuestionRound = usedAskUser || wrotePlainQuestions;\n return {\n isQuestionRound,\n usedAskUser,\n wrotePlainQuestions,\n handledCleanly: isQuestionRound && usedAskUser && !wrotePlainQuestions,\n };\n}\n"
|
|
6
7
|
],
|
|
7
|
-
"mappings": "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",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "F81601C4D787F3A264756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capdiem/pi-ask-user",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An interactive ask_user form (Custom UI) for the Pi coding agent: turns grilling-skill questions into selectable options with recommended-answer hints — TUI full form, RPC sequential dialogs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"pi": {
|