@agimon-ai/doompi-user-feedback 0.0.1-alpha.2 → 0.0.1-alpha.20

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.
Files changed (56) hide show
  1. package/README.md +95 -4
  2. package/dist/adapters/doom/voiceQuestionHandoff.cjs +2 -2
  3. package/dist/adapters/doom/voiceQuestionHandoff.cjs.map +1 -1
  4. package/dist/adapters/doom/voiceQuestionHandoff.mjs +2 -2
  5. package/dist/adapters/doom/voiceQuestionHandoff.mjs.map +1 -1
  6. package/dist/adapters/pi/askUserQuestionAdapter.cjs +2 -2
  7. package/dist/adapters/pi/askUserQuestionAdapter.cjs.map +1 -1
  8. package/dist/adapters/pi/askUserQuestionAdapter.mjs +2 -2
  9. package/dist/adapters/pi/askUserQuestionAdapter.mjs.map +1 -1
  10. package/dist/adapters/pi/extension.cjs +2 -0
  11. package/dist/adapters/pi/extension.cjs.map +1 -0
  12. package/dist/adapters/pi/extension.d.cts +8 -0
  13. package/dist/adapters/pi/extension.d.cts.map +1 -0
  14. package/dist/adapters/pi/extension.d.mts +8 -0
  15. package/dist/adapters/pi/extension.d.mts.map +1 -0
  16. package/dist/adapters/pi/extension.mjs +2 -0
  17. package/dist/adapters/pi/extension.mjs.map +1 -0
  18. package/dist/adapters/pi/reconcileAdapter.cjs +1 -1
  19. package/dist/adapters/pi/reconcileAdapter.cjs.map +1 -1
  20. package/dist/adapters/pi/reconcileAdapter.mjs +1 -1
  21. package/dist/adapters/pi/reconcileAdapter.mjs.map +1 -1
  22. package/dist/extensions/pi.cjs +1 -2
  23. package/dist/extensions/pi.d.cts +2 -6
  24. package/dist/extensions/pi.d.mts +2 -6
  25. package/dist/extensions/pi.mjs +1 -2
  26. package/dist/index.cjs +0 -1
  27. package/dist/index.d.cts +3 -4
  28. package/dist/index.d.mts +3 -4
  29. package/dist/index.mjs +1 -1
  30. package/dist/schemas/questionnaire.d.cts +18 -0
  31. package/dist/schemas/questionnaire.d.cts.map +1 -0
  32. package/dist/schemas/questionnaire.d.mts +18 -0
  33. package/dist/schemas/questionnaire.d.mts.map +1 -0
  34. package/dist/services/questionnaireCoordinator.cjs +1 -1
  35. package/dist/services/questionnaireCoordinator.cjs.map +1 -1
  36. package/dist/services/questionnaireCoordinator.mjs +1 -1
  37. package/dist/services/questionnaireCoordinator.mjs.map +1 -1
  38. package/dist/types/questionnaire.d.cts +31 -0
  39. package/dist/types/questionnaire.d.cts.map +1 -0
  40. package/dist/types/questionnaire.d.mts +31 -0
  41. package/dist/types/questionnaire.d.mts.map +1 -0
  42. package/package.json +26 -18
  43. package/dist/events.cjs +0 -1
  44. package/dist/events.d.cts +0 -2
  45. package/dist/events.d.mts +0 -2
  46. package/dist/events.mjs +0 -1
  47. package/dist/extensions/pi.cjs.map +0 -1
  48. package/dist/extensions/pi.d.cts.map +0 -1
  49. package/dist/extensions/pi.d.mts.map +0 -1
  50. package/dist/extensions/pi.mjs.map +0 -1
  51. package/dist/schemas/events.cjs +0 -1
  52. package/dist/schemas/events.d.cts +0 -22
  53. package/dist/schemas/events.d.cts.map +0 -1
  54. package/dist/schemas/events.d.mts +0 -22
  55. package/dist/schemas/events.d.mts.map +0 -1
  56. package/dist/schemas/events.mjs +0 -1
package/README.md CHANGED
@@ -1,10 +1,97 @@
1
1
  # @agimon-ai/doompi-user-feedback
2
2
 
3
- An independent DoomPi extension for structured `ask_user_question` interactions.
3
+ Structured user questions that wait for an answer in interactive Pi sessions, with an autonomous
4
+ Voice handoff.
4
5
 
5
- The implementation is written from scratch for Pi 0.84.1. It retains the established `rpiv:ask-user:prompt` and `rpiv:ask-user:blocked` channel names so existing DoomPi consumers can migrate without changing their listeners. It has no dependency on `@juicesharp/rpiv-ask-user-question`, `@juicesharp/rpiv-i18n`, or other Juicesharp packages.
6
+ Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
6
7
 
7
- When DoomPi autonomous voice mode is active, the extension narrates the questions without opening a modal questionnaire. The spoken response returns through Doom Voice as the next user message.
8
+ The `ask_user_question` tool presents concrete options instead of asking the model to continue on a
9
+ guess.
10
+
11
+ > **Alpha:** tool and Voice-handoff contracts may change between releases.
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 22.19.0 or newer
16
+ - Pi 0.84.2 and Pi TUI 0.84.2
17
+
18
+ ## Install
19
+
20
+ Add the package to a DoomPi layer. DoomPi preserves authored layer and package occurrence order,
21
+ while activating this package once per canonical resolved path:
22
+
23
+ ```yaml
24
+ layers:
25
+ feedback:
26
+ packages: ['@agimon-ai/doompi-user-feedback']
27
+ ```
28
+
29
+ For standalone Pi:
30
+
31
+ ```bash
32
+ pi install npm:@agimon-ai/doompi-user-feedback
33
+ ```
34
+
35
+ Pi loads the package through `package.json.pi.extensions`. Each factory invocation owns one package
36
+ instance. Session replacement resets its state, and shutdown removes its registrations.
37
+
38
+ ## Ask a question
39
+
40
+ ```json
41
+ {
42
+ "questions": [
43
+ {
44
+ "header": "Storage",
45
+ "question": "Where should session results be stored?",
46
+ "options": [
47
+ {
48
+ "label": "Project files",
49
+ "description": "Easy to inspect and commit, but visible in the repository."
50
+ },
51
+ {
52
+ "label": "User state",
53
+ "description": "Keeps generated state outside the repository."
54
+ }
55
+ ],
56
+ "multiSelect": false
57
+ }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ A call accepts 1 to 4 questions. Each question requires 2 to 4 options. Headers are at most 16
63
+ characters; option labels are at most 60 characters. Use `multiSelect: true` when several choices
64
+ are valid. An option can include a Markdown `preview` for meaningful visual comparison.
65
+
66
+ The tool result contains readable answer text plus structured `details.answers`; cancellation sets
67
+ `cancelled: true`. Pressing Escape declines the questionnaire. Do not add an `Other` option because
68
+ the UI supplies a custom-text row.
69
+
70
+ ## UI, RPC, and headless behavior
71
+
72
+ The tool requires an interactive UI. It uses the TUI questionnaire when available and can use Pi's
73
+ dialog or RPC UI bridge. A headless context without either UI returns a structured `no_ui`
74
+ cancellation rather than waiting forever.
75
+
76
+ Questionnaires are coordinated one at a time. Cancellation, session replacement, or shutdown
77
+ aborts the active prompt and settles queued work.
78
+
79
+ ## Autonomous Voice handoff
80
+
81
+ When autonomous Voice mode is active, the extension formats the questions as plain text, requests
82
+ narration, and returns `terminate: true`. The current turn ends while waiting; the next spoken
83
+ answer arrives as a normal user message rather than a synthetic questionnaire selection.
84
+
85
+ The handoff uses the shared `doom/minor-mode-catalog` and `doom/narration` Cordis services. If either
86
+ provider unloads, User Feedback immediately falls back to its interactive questionnaire without
87
+ retaining the old provider.
88
+
89
+ ## Public API
90
+
91
+ Question and result types are available from the package root. Cross-extension ask-user events and
92
+ Voice services come from `@agimon-ai/doompi-extension-contracts`; consumers should use those Cordis
93
+ contracts. Pi loads the default export of `/extensions/pi` through package metadata, so installation
94
+ does not require a manual registration call.
8
95
 
9
96
  ## Development
10
97
 
@@ -15,4 +102,8 @@ pnpm test
15
102
  pnpm lint
16
103
  ```
17
104
 
18
- The Pi extension entrypoint is `@agimon-ai/doompi-user-feedback/extensions/pi`.
105
+ Maintained by [Agimon](https://agimon.ai/about).
106
+
107
+ ## License
108
+
109
+ MIT
@@ -1,5 +1,5 @@
1
- const e=`The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.`;function t(e){return e.source===`@agimon-ai/doompi-voice`&&e.id===`voice-auto`}function n(e){let t=e.questions.length>1;return e.questions.map((e,n)=>[`${t?`${n+1}. `:``}${e.question}${e.multiSelect?` (Select all that apply.)`:``}`,...e.options.map((e,t)=>` ${t+1}. ${e.label}`),` Type something.`].join(`
1
+ const e=require("../../services/voiceQuestionNarration.cjs");let t=require("@agimon-ai/doompi-extension-contracts/narration");const n=`The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.`;function r(e){return e.descriptor.source===t.DOOM_VOICE_SOURCE&&e.descriptor.id===t.DOOM_VOICE_AUTO_MODE_ID&&e.state.activation===`active`}function i(e){let t=e.questions.length>1;return e.questions.map((e,n)=>[`${t?`${n+1}. `:``}${e.question}${e.multiSelect?` (Select all that apply.)`:``}`,...e.options.map((e,t)=>` ${t+1}. ${e.label}`),` Type something.`].join(`
2
2
  `)).join(`
3
3
 
4
- `)}var r=class{modes;narration;buildNarration;constructor(e,t,n){this.modes=e,this.narration=t,this.buildNarration=n}handoff(e){if(this.modes.getStatuses().some(t))return this.narration.narrate(this.buildNarration(e)),{answers:[],cancelled:!1,delivery:`voice`,awaitingResponse:!0,voicePrompt:n(e)}}};function i(t){return{content:[{type:`text`,text:e}],details:t,terminate:!0}}exports.DOOM_VOICE_AUTO_MODE_ID=`voice-auto`,exports.DOOM_VOICE_SOURCE=`@agimon-ai/doompi-voice`,exports.VOICE_WAIT_MESSAGE=e,exports.VoiceQuestionHandoff=r,exports.buildVoiceToolResult=i,exports.formatVoiceQuestionPrompt=n;
4
+ `)}var a=class{modes;narration;buildNarration;constructor(t,n,r=e.buildVoiceQuestionNarration){this.modes=t,this.narration=n,this.buildNarration=r}async handoff(e){if(!this.modes.list().some(r))return;let n=(0,t.createNarrationRequest)(this.buildNarration(e));if(n)return await this.narration.request(n),{answers:[],cancelled:!1,delivery:`voice`,awaitingResponse:!0,voicePrompt:i(e)}}};function o(e,t){return new a(e,t)}function s(e){let t=e.voicePrompt?.trim();return{content:[{type:`text`,text:t?`${t}\n\n${n}`:n}],details:e,terminate:!0}}exports.VOICE_WAIT_MESSAGE=n,exports.VoiceQuestionHandoff=a,exports.buildVoiceToolResult=s,exports.createVoiceQuestionHandoff=o,exports.formatVoiceQuestionPrompt=i;
5
5
  //# sourceMappingURL=voiceQuestionHandoff.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"voiceQuestionHandoff.cjs","names":[],"sources":["../../../src/adapters/doom/voiceQuestionHandoff.ts"],"sourcesContent":["import type { DoomModeStatusRegistry, DoomModeStatus } from '@agimon-ai/doompi-extension-contracts/mode';\nimport type { NarrationRequester } from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { QuestionParams } from '../../schemas/questionnaire.js';\nimport type { QuestionnaireResult, ToolTextResult } from '../../types/questionnaire.js';\n\nexport const DOOM_VOICE_SOURCE = '@agimon-ai/doompi-voice';\nexport const DOOM_VOICE_AUTO_MODE_ID = 'voice-auto';\nexport const VOICE_WAIT_MESSAGE =\n \"The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.\";\n\nfunction isAutonomousVoice(status: DoomModeStatus): boolean {\n return status.source === DOOM_VOICE_SOURCE && status.id === DOOM_VOICE_AUTO_MODE_ID;\n}\n\nexport function formatVoiceQuestionPrompt(params: QuestionParams): string {\n const numberQuestions = params.questions.length > 1;\n return params.questions\n .map((question, questionIndex) => {\n const heading = `${numberQuestions ? `${questionIndex + 1}. ` : ''}${question.question}${question.multiSelect ? ' (Select all that apply.)' : ''}`;\n const options = question.options.map((option, optionIndex) => ` ${optionIndex + 1}. ${option.label}`);\n return [heading, ...options, ' Type something.'].join('\\n');\n })\n .join('\\n\\n');\n}\n\nexport class VoiceQuestionHandoff {\n constructor(\n private readonly modes: Pick<DoomModeStatusRegistry, 'getStatuses'>,\n private readonly narration: NarrationRequester,\n private readonly buildNarration: (params: QuestionParams) => string,\n ) {}\n\n handoff(params: QuestionParams): QuestionnaireResult | undefined {\n if (!this.modes.getStatuses().some(isAutonomousVoice)) return undefined;\n this.narration.narrate(this.buildNarration(params));\n return {\n answers: [],\n cancelled: false,\n delivery: 'voice',\n awaitingResponse: true,\n voicePrompt: formatVoiceQuestionPrompt(params),\n };\n }\n}\n\nexport function buildVoiceToolResult(details: QuestionnaireResult): ToolTextResult {\n return {\n content: [{ type: 'text', text: VOICE_WAIT_MESSAGE }],\n details,\n terminate: true,\n };\n}\n"],"mappings":"AAKA,MAEa,EACX,iJAEF,SAAS,EAAkB,EAAiC,CAC1D,OAAO,EAAO,SAAA,2BAAgC,EAAO,KAAA,YACvD,CAEA,SAAgB,EAA0B,EAAgC,CACxE,IAAM,EAAkB,EAAO,UAAU,OAAS,EAClD,OAAO,EAAO,UACX,KAAK,EAAU,IAGP,CAAC,GAFW,EAAkB,GAAG,EAAgB,EAAE,IAAM,KAAK,EAAS,WAAW,EAAS,YAAc,4BAA8B,KAE7H,GADD,EAAS,QAAQ,KAAK,EAAQ,IAAgB,KAAK,EAAc,EAAE,IAAI,EAAO,OACpE,EAAG,mBAAmB,CAAC,CAAC,KAAK;CAAI,CAC5D,CAAC,CACD,KAAK;;CAAM,CAChB,CAEA,IAAa,EAAb,KAAkC,CAEb,MACA,UACA,eAHnB,YACE,EACA,EACA,EACA,CAHiB,KAAA,MAAA,EACA,KAAA,UAAA,EACA,KAAA,eAAA,CAChB,CAEH,QAAQ,EAAyD,CAC1D,QAAK,MAAM,YAAY,CAAC,CAAC,KAAK,CAAiB,EAEpD,OADA,KAAK,UAAU,QAAQ,KAAK,eAAe,CAAM,CAAC,EAC3C,CACL,QAAS,CAAC,EACV,UAAW,GACX,SAAU,QACV,iBAAkB,GAClB,YAAa,EAA0B,CAAM,CAC/C,CACF,CACF,EAEA,SAAgB,EAAqB,EAA8C,CACjF,MAAO,CACL,QAAS,CAAC,CAAE,KAAM,OAAQ,KAAM,CAAmB,CAAC,EACpD,UACA,UAAW,EACb,CACF"}
1
+ {"version":3,"file":"voiceQuestionHandoff.cjs","names":["DOOM_VOICE_SOURCE","DOOM_VOICE_AUTO_MODE_ID","buildVoiceQuestionNarration","createNarrationRequest"],"sources":["../../../src/adapters/doom/voiceQuestionHandoff.ts"],"sourcesContent":["import type { MinorModeCatalogService, MinorModeRecord } from '@agimon-ai/doompi-extension-contracts/mode';\nimport {\n createNarrationRequest,\n type DoomNarrationService,\n DOOM_VOICE_AUTO_MODE_ID,\n DOOM_VOICE_SOURCE,\n} from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { QuestionParams } from '../../schemas/questionnaire.js';\nimport { buildVoiceQuestionNarration } from '../../services/voiceQuestionNarration.js';\nimport type { QuestionnaireResult, ToolTextResult } from '../../types/questionnaire.js';\n\nexport const VOICE_WAIT_MESSAGE =\n \"The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.\";\n\nfunction isAutonomousVoice(record: MinorModeRecord): boolean {\n return (\n record.descriptor.source === DOOM_VOICE_SOURCE &&\n record.descriptor.id === DOOM_VOICE_AUTO_MODE_ID &&\n record.state.activation === 'active'\n );\n}\n\nexport function formatVoiceQuestionPrompt(params: QuestionParams): string {\n const numberQuestions = params.questions.length > 1;\n return params.questions\n .map((question, questionIndex) => {\n const heading = `${numberQuestions ? `${questionIndex + 1}. ` : ''}${question.question}${question.multiSelect ? ' (Select all that apply.)' : ''}`;\n const options = question.options.map((option, optionIndex) => ` ${optionIndex + 1}. ${option.label}`);\n return [heading, ...options, ' Type something.'].join('\\n');\n })\n .join('\\n\\n');\n}\n\nexport class VoiceQuestionHandoff {\n constructor(\n private readonly modes: Pick<MinorModeCatalogService, 'list'>,\n private readonly narration: Pick<DoomNarrationService, 'request'>,\n private readonly buildNarration: (params: QuestionParams) => string = buildVoiceQuestionNarration,\n ) {}\n\n async handoff(params: QuestionParams): Promise<QuestionnaireResult | undefined> {\n if (!this.modes.list().some(isAutonomousVoice)) return undefined;\n const narration = createNarrationRequest(this.buildNarration(params));\n if (!narration) return undefined;\n await this.narration.request(narration);\n return {\n answers: [],\n cancelled: false,\n delivery: 'voice',\n awaitingResponse: true,\n voicePrompt: formatVoiceQuestionPrompt(params),\n };\n }\n}\n\n/** Create a handoff from services whose lifecycle is owned by a Cordis injection. */\nexport function createVoiceQuestionHandoff(\n modes: Pick<MinorModeCatalogService, 'list'>,\n narration: Pick<DoomNarrationService, 'request'>,\n): VoiceQuestionHandoff {\n return new VoiceQuestionHandoff(modes, narration);\n}\n\nexport function buildVoiceToolResult(details: QuestionnaireResult): ToolTextResult {\n const prompt = details.voicePrompt?.trim();\n return {\n content: [{ type: 'text', text: prompt ? `${prompt}\\n\\n${VOICE_WAIT_MESSAGE}` : VOICE_WAIT_MESSAGE }],\n details,\n terminate: true,\n };\n}\n"],"mappings":"8HAWA,MAAa,EACX,iJAEF,SAAS,EAAkB,EAAkC,CAC3D,OACE,EAAO,WAAW,SAAWA,EAAAA,mBAC7B,EAAO,WAAW,KAAOC,EAAAA,yBACzB,EAAO,MAAM,aAAe,QAEhC,CAEA,SAAgB,EAA0B,EAAgC,CACxE,IAAM,EAAkB,EAAO,UAAU,OAAS,EAClD,OAAO,EAAO,UACX,KAAK,EAAU,IAGP,CAAC,GAFW,EAAkB,GAAG,EAAgB,EAAE,IAAM,KAAK,EAAS,WAAW,EAAS,YAAc,4BAA8B,KAE7H,GADD,EAAS,QAAQ,KAAK,EAAQ,IAAgB,KAAK,EAAc,EAAE,IAAI,EAAO,OACpE,EAAG,mBAAmB,CAAC,CAAC,KAAK;CAAI,CAC5D,CAAC,CACD,KAAK;;CAAM,CAChB,CAEA,IAAa,EAAb,KAAkC,CAEb,MACA,UACA,eAHnB,YACE,EACA,EACA,EAAsEC,EAAAA,4BACtE,CAHiB,KAAA,MAAA,EACA,KAAA,UAAA,EACA,KAAA,eAAA,CAChB,CAEH,MAAM,QAAQ,EAAkE,CAC9E,GAAI,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC,KAAK,CAAiB,EAAG,OAChD,IAAM,GAAA,EAAYC,EAAAA,uBAAAA,CAAuB,KAAK,eAAe,CAAM,CAAC,EAC/D,KAEL,OADA,MAAM,KAAK,UAAU,QAAQ,CAAS,EAC/B,CACL,QAAS,CAAC,EACV,UAAW,GACX,SAAU,QACV,iBAAkB,GAClB,YAAa,EAA0B,CAAM,CAC/C,CACF,CACF,EAGA,SAAgB,EACd,EACA,EACsB,CACtB,OAAO,IAAI,EAAqB,EAAO,CAAS,CAClD,CAEA,SAAgB,EAAqB,EAA8C,CACjF,IAAM,EAAS,EAAQ,aAAa,KAAK,EACzC,MAAO,CACL,QAAS,CAAC,CAAE,KAAM,OAAQ,KAAM,EAAS,GAAG,EAAO,MAAM,IAAuB,CAAmB,CAAC,EACpG,UACA,UAAW,EACb,CACF"}
@@ -1,5 +1,5 @@
1
- const e=`@agimon-ai/doompi-voice`,t=`voice-auto`,n=`The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.`;function r(e){return e.source===`@agimon-ai/doompi-voice`&&e.id===`voice-auto`}function i(e){let t=e.questions.length>1;return e.questions.map((e,n)=>[`${t?`${n+1}. `:``}${e.question}${e.multiSelect?` (Select all that apply.)`:``}`,...e.options.map((e,t)=>` ${t+1}. ${e.label}`),` Type something.`].join(`
1
+ import{buildVoiceQuestionNarration as e}from"../../services/voiceQuestionNarration.mjs";import{DOOM_VOICE_AUTO_MODE_ID as t,DOOM_VOICE_SOURCE as n,createNarrationRequest as r}from"@agimon-ai/doompi-extension-contracts/narration";const i=`The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.`;function a(e){return e.descriptor.source===n&&e.descriptor.id===t&&e.state.activation===`active`}function o(e){let t=e.questions.length>1;return e.questions.map((e,n)=>[`${t?`${n+1}. `:``}${e.question}${e.multiSelect?` (Select all that apply.)`:``}`,...e.options.map((e,t)=>` ${t+1}. ${e.label}`),` Type something.`].join(`
2
2
  `)).join(`
3
3
 
4
- `)}var a=class{modes;narration;buildNarration;constructor(e,t,n){this.modes=e,this.narration=t,this.buildNarration=n}handoff(e){if(this.modes.getStatuses().some(r))return this.narration.narrate(this.buildNarration(e)),{answers:[],cancelled:!1,delivery:`voice`,awaitingResponse:!0,voicePrompt:i(e)}}};function o(e){return{content:[{type:`text`,text:n}],details:e,terminate:!0}}export{t as DOOM_VOICE_AUTO_MODE_ID,e as DOOM_VOICE_SOURCE,n as VOICE_WAIT_MESSAGE,a as VoiceQuestionHandoff,o as buildVoiceToolResult,i as formatVoiceQuestionPrompt};
4
+ `)}var s=class{modes;narration;buildNarration;constructor(t,n,r=e){this.modes=t,this.narration=n,this.buildNarration=r}async handoff(e){if(!this.modes.list().some(a))return;let t=r(this.buildNarration(e));if(t)return await this.narration.request(t),{answers:[],cancelled:!1,delivery:`voice`,awaitingResponse:!0,voicePrompt:o(e)}}};function c(e,t){return new s(e,t)}function l(e){let t=e.voicePrompt?.trim();return{content:[{type:`text`,text:t?`${t}\n\n${i}`:i}],details:e,terminate:!0}}export{i as VOICE_WAIT_MESSAGE,s as VoiceQuestionHandoff,l as buildVoiceToolResult,c as createVoiceQuestionHandoff,o as formatVoiceQuestionPrompt};
5
5
  //# sourceMappingURL=voiceQuestionHandoff.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"voiceQuestionHandoff.mjs","names":[],"sources":["../../../src/adapters/doom/voiceQuestionHandoff.ts"],"sourcesContent":["import type { DoomModeStatusRegistry, DoomModeStatus } from '@agimon-ai/doompi-extension-contracts/mode';\nimport type { NarrationRequester } from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { QuestionParams } from '../../schemas/questionnaire.js';\nimport type { QuestionnaireResult, ToolTextResult } from '../../types/questionnaire.js';\n\nexport const DOOM_VOICE_SOURCE = '@agimon-ai/doompi-voice';\nexport const DOOM_VOICE_AUTO_MODE_ID = 'voice-auto';\nexport const VOICE_WAIT_MESSAGE =\n \"The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.\";\n\nfunction isAutonomousVoice(status: DoomModeStatus): boolean {\n return status.source === DOOM_VOICE_SOURCE && status.id === DOOM_VOICE_AUTO_MODE_ID;\n}\n\nexport function formatVoiceQuestionPrompt(params: QuestionParams): string {\n const numberQuestions = params.questions.length > 1;\n return params.questions\n .map((question, questionIndex) => {\n const heading = `${numberQuestions ? `${questionIndex + 1}. ` : ''}${question.question}${question.multiSelect ? ' (Select all that apply.)' : ''}`;\n const options = question.options.map((option, optionIndex) => ` ${optionIndex + 1}. ${option.label}`);\n return [heading, ...options, ' Type something.'].join('\\n');\n })\n .join('\\n\\n');\n}\n\nexport class VoiceQuestionHandoff {\n constructor(\n private readonly modes: Pick<DoomModeStatusRegistry, 'getStatuses'>,\n private readonly narration: NarrationRequester,\n private readonly buildNarration: (params: QuestionParams) => string,\n ) {}\n\n handoff(params: QuestionParams): QuestionnaireResult | undefined {\n if (!this.modes.getStatuses().some(isAutonomousVoice)) return undefined;\n this.narration.narrate(this.buildNarration(params));\n return {\n answers: [],\n cancelled: false,\n delivery: 'voice',\n awaitingResponse: true,\n voicePrompt: formatVoiceQuestionPrompt(params),\n };\n }\n}\n\nexport function buildVoiceToolResult(details: QuestionnaireResult): ToolTextResult {\n return {\n content: [{ type: 'text', text: VOICE_WAIT_MESSAGE }],\n details,\n terminate: true,\n };\n}\n"],"mappings":"AAKA,MAAa,EAAoB,0BACpB,EAA0B,aAC1B,EACX,iJAEF,SAAS,EAAkB,EAAiC,CAC1D,OAAO,EAAO,SAAA,2BAAgC,EAAO,KAAA,YACvD,CAEA,SAAgB,EAA0B,EAAgC,CACxE,IAAM,EAAkB,EAAO,UAAU,OAAS,EAClD,OAAO,EAAO,UACX,KAAK,EAAU,IAGP,CAAC,GAFW,EAAkB,GAAG,EAAgB,EAAE,IAAM,KAAK,EAAS,WAAW,EAAS,YAAc,4BAA8B,KAE7H,GADD,EAAS,QAAQ,KAAK,EAAQ,IAAgB,KAAK,EAAc,EAAE,IAAI,EAAO,OACpE,EAAG,mBAAmB,CAAC,CAAC,KAAK;CAAI,CAC5D,CAAC,CACD,KAAK;;CAAM,CAChB,CAEA,IAAa,EAAb,KAAkC,CAEb,MACA,UACA,eAHnB,YACE,EACA,EACA,EACA,CAHiB,KAAA,MAAA,EACA,KAAA,UAAA,EACA,KAAA,eAAA,CAChB,CAEH,QAAQ,EAAyD,CAC1D,QAAK,MAAM,YAAY,CAAC,CAAC,KAAK,CAAiB,EAEpD,OADA,KAAK,UAAU,QAAQ,KAAK,eAAe,CAAM,CAAC,EAC3C,CACL,QAAS,CAAC,EACV,UAAW,GACX,SAAU,QACV,iBAAkB,GAClB,YAAa,EAA0B,CAAM,CAC/C,CACF,CACF,EAEA,SAAgB,EAAqB,EAA8C,CACjF,MAAO,CACL,QAAS,CAAC,CAAE,KAAM,OAAQ,KAAM,CAAmB,CAAC,EACpD,UACA,UAAW,EACb,CACF"}
1
+ {"version":3,"file":"voiceQuestionHandoff.mjs","names":[],"sources":["../../../src/adapters/doom/voiceQuestionHandoff.ts"],"sourcesContent":["import type { MinorModeCatalogService, MinorModeRecord } from '@agimon-ai/doompi-extension-contracts/mode';\nimport {\n createNarrationRequest,\n type DoomNarrationService,\n DOOM_VOICE_AUTO_MODE_ID,\n DOOM_VOICE_SOURCE,\n} from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { QuestionParams } from '../../schemas/questionnaire.js';\nimport { buildVoiceQuestionNarration } from '../../services/voiceQuestionNarration.js';\nimport type { QuestionnaireResult, ToolTextResult } from '../../types/questionnaire.js';\n\nexport const VOICE_WAIT_MESSAGE =\n \"The questions were spoken through autonomous voice. Stop now and wait for the user's next message; it will arrive as an ordinary user message.\";\n\nfunction isAutonomousVoice(record: MinorModeRecord): boolean {\n return (\n record.descriptor.source === DOOM_VOICE_SOURCE &&\n record.descriptor.id === DOOM_VOICE_AUTO_MODE_ID &&\n record.state.activation === 'active'\n );\n}\n\nexport function formatVoiceQuestionPrompt(params: QuestionParams): string {\n const numberQuestions = params.questions.length > 1;\n return params.questions\n .map((question, questionIndex) => {\n const heading = `${numberQuestions ? `${questionIndex + 1}. ` : ''}${question.question}${question.multiSelect ? ' (Select all that apply.)' : ''}`;\n const options = question.options.map((option, optionIndex) => ` ${optionIndex + 1}. ${option.label}`);\n return [heading, ...options, ' Type something.'].join('\\n');\n })\n .join('\\n\\n');\n}\n\nexport class VoiceQuestionHandoff {\n constructor(\n private readonly modes: Pick<MinorModeCatalogService, 'list'>,\n private readonly narration: Pick<DoomNarrationService, 'request'>,\n private readonly buildNarration: (params: QuestionParams) => string = buildVoiceQuestionNarration,\n ) {}\n\n async handoff(params: QuestionParams): Promise<QuestionnaireResult | undefined> {\n if (!this.modes.list().some(isAutonomousVoice)) return undefined;\n const narration = createNarrationRequest(this.buildNarration(params));\n if (!narration) return undefined;\n await this.narration.request(narration);\n return {\n answers: [],\n cancelled: false,\n delivery: 'voice',\n awaitingResponse: true,\n voicePrompt: formatVoiceQuestionPrompt(params),\n };\n }\n}\n\n/** Create a handoff from services whose lifecycle is owned by a Cordis injection. */\nexport function createVoiceQuestionHandoff(\n modes: Pick<MinorModeCatalogService, 'list'>,\n narration: Pick<DoomNarrationService, 'request'>,\n): VoiceQuestionHandoff {\n return new VoiceQuestionHandoff(modes, narration);\n}\n\nexport function buildVoiceToolResult(details: QuestionnaireResult): ToolTextResult {\n const prompt = details.voicePrompt?.trim();\n return {\n content: [{ type: 'text', text: prompt ? `${prompt}\\n\\n${VOICE_WAIT_MESSAGE}` : VOICE_WAIT_MESSAGE }],\n details,\n terminate: true,\n };\n}\n"],"mappings":"qOAWA,MAAa,EACX,iJAEF,SAAS,EAAkB,EAAkC,CAC3D,OACE,EAAO,WAAW,SAAW,GAC7B,EAAO,WAAW,KAAO,GACzB,EAAO,MAAM,aAAe,QAEhC,CAEA,SAAgB,EAA0B,EAAgC,CACxE,IAAM,EAAkB,EAAO,UAAU,OAAS,EAClD,OAAO,EAAO,UACX,KAAK,EAAU,IAGP,CAAC,GAFW,EAAkB,GAAG,EAAgB,EAAE,IAAM,KAAK,EAAS,WAAW,EAAS,YAAc,4BAA8B,KAE7H,GADD,EAAS,QAAQ,KAAK,EAAQ,IAAgB,KAAK,EAAc,EAAE,IAAI,EAAO,OACpE,EAAG,mBAAmB,CAAC,CAAC,KAAK;CAAI,CAC5D,CAAC,CACD,KAAK;;CAAM,CAChB,CAEA,IAAa,EAAb,KAAkC,CAEb,MACA,UACA,eAHnB,YACE,EACA,EACA,EAAsE,EACtE,CAHiB,KAAA,MAAA,EACA,KAAA,UAAA,EACA,KAAA,eAAA,CAChB,CAEH,MAAM,QAAQ,EAAkE,CAC9E,GAAI,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC,KAAK,CAAiB,EAAG,OAChD,IAAM,EAAY,EAAuB,KAAK,eAAe,CAAM,CAAC,EAC/D,KAEL,OADA,MAAM,KAAK,UAAU,QAAQ,CAAS,EAC/B,CACL,QAAS,CAAC,EACV,UAAW,GACX,SAAU,QACV,iBAAkB,GAClB,YAAa,EAA0B,CAAM,CAC/C,CACF,CACF,EAGA,SAAgB,EACd,EACA,EACsB,CACtB,OAAO,IAAI,EAAqB,EAAO,CAAS,CAClD,CAEA,SAAgB,EAAqB,EAA8C,CACjF,IAAM,EAAS,EAAQ,aAAa,KAAK,EACzC,MAAO,CACL,QAAS,CAAC,CAAE,KAAM,OAAQ,KAAM,EAAS,GAAG,EAAO,MAAM,IAAuB,CAAmB,CAAC,EACpG,UACA,UAAW,EACb,CACF"}
@@ -1,4 +1,4 @@
1
- const e=require("../doom/voiceQuestionHandoff.cjs"),t=require("../../schemas/questionnaire.cjs"),n=require("../../services/responseService.cjs"),r=require("../../services/validationService.cjs"),i=require("../config/configAdapter.cjs"),a=require("./rpcQuestionnaireAdapter.cjs");let o=require("@agimon-ai/doompi-extension-contracts"),s=require("@agimon-ai/doompi-ui/toolChrome");const c=`ask_user_question`,l=`Ask the user up to 4 structured questions (2-4 options each) when requirements are ambiguous`,u=[`Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to 4 questions per invocation.`,`Each question MUST have 2-4 options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended "Type something." row on every question, or press Esc to abandon the questionnaire. Do NOT author "Other" or "Type something." labels yourself — reserved labels are rejected at runtime.`,`Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append "(Recommended)" to its label.`,`Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.`];function d(e,t){let n={questions:t.questions.map(e=>({question:e.question,header:e.header,multiSelect:e.multiSelect??!1,options:e.options.map(e=>({label:e.label,description:e.description,hasPreview:!!e.preview}))}))};(0,o.publishAskUserPrompt)(e.events,n)}function f(e,t){let n={active:t};(0,o.publishAskUserBlocked)(e.events,n)}function p(e){let t=e.ui;return typeof t.select==`function`&&typeof t.input==`function`}function m(o,m){let h=i.loadUserFeedbackConfig().guidance;o.registerTool({name:c,label:`Ask User Question`,renderShell:`self`,description:`Ask the user one or more structured questions during execution. Use when you need to:
1
+ const e=require("../doom/voiceQuestionHandoff.cjs"),t=require("../../schemas/questionnaire.cjs"),n=require("../../services/responseService.cjs"),r=require("../../services/validationService.cjs"),i=require("../config/configAdapter.cjs"),a=require("./rpcQuestionnaireAdapter.cjs");let o=require("@agimon-ai/doompi-extension-contracts/ask-user"),s=require("@agimon-ai/doompi-ui/toolChrome");const c=`ask_user_question`,l=`Ask the user up to 4 structured questions (2-4 options each) when requirements are ambiguous`,u=[`Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to 4 questions per invocation.`,`Each question MUST have 2-4 options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended "Type something." row on every question, or press Esc to abandon the questionnaire. Do NOT author "Other" or "Type something." labels yourself — reserved labels are rejected at runtime.`,`Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append "(Recommended)" to its label.`,`Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.`];function d(e,t){let n={questions:t.questions.map(e=>({question:e.question,header:e.header,multiSelect:e.multiSelect??!1,options:e.options.map(e=>({label:e.label,description:e.description,hasPreview:!!e.preview}))}))};e.emit(o.DOOM_ASK_USER_PROMPT_EVENT,n)}function f(e,t){let n={active:t};e.emit(o.DOOM_ASK_USER_BLOCKED_EVENT,n)}function p(e){let t=e.ui;return typeof t.select==`function`&&typeof t.input==`function`}function m(o,m,h){let g=i.loadUserFeedbackConfig().guidance;o.registerTool({name:c,label:`Ask User Question`,renderShell:`self`,description:`Ask the user one or more structured questions during execution. Use when you need to:
2
2
  1. Gather user preferences or requirements
3
3
  2. Clarify ambiguous instructions
4
4
  3. Get decisions on implementation choices as you work
@@ -10,6 +10,6 @@ Usage notes:
10
10
  - If you recommend a specific option, make that the first option and add "(Recommended)" at the end of the label.
11
11
 
12
12
  Preview feature:
13
- Use the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,promptSnippet:h?.promptSnippet??l,promptGuidelines:h?.promptGuidelines??u,parameters:t.QuestionParamsSchema,executionMode:`sequential`,async execute(t,s,c,l,u){let h=s;if(!u.hasUI)return n.buildToolResult(`Error: UI not available (running in non-interactive mode)`,{answers:[],cancelled:!0,error:`no_ui`});let g=r.validateQuestionnaire(h);if(!g.ok)return n.buildToolResult(g.message,{answers:[],cancelled:!0,error:g.error});try{let t=await m.enqueue(async({signal:e,reportProgress:t})=>{d(o,h);let n=m.tryVoice?.(h);if(n)return n;let r=!0,s=()=>{r&&(r=!1,f(o,!1))};e.addEventListener(`abort`,s,{once:!0}),f(o,!0);try{if(u.mode===`rpc`&&p(u))return await a.runRpcQuestionnaire(u,h,e,t);let n=await m.runTui(u,h,i.resolveCollapseKey(i.loadUserFeedbackConfig()),e,t);return n===void 0?p(u)?await a.runRpcQuestionnaire(u,h,e,t):{answers:[],cancelled:!0,error:`no_custom_ui`}:n}finally{e.removeEventListener(`abort`,s),s()}},c);return t.delivery===`voice`?e.buildVoiceToolResult(t):t.error===`no_custom_ui`?n.buildToolResult(`Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.`,t):n.buildQuestionnaireResponse(t,h)}catch(e){let t=e instanceof Error?e.message:String(e);return n.buildToolResult(`Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead. (cause: ${t})`,{answers:[],cancelled:!0,error:`session_load_failed`})}},renderCall(e,t){let n=e,r=n.questions.map(e=>e.header).join(`, `),i=`${n.questions.length} question${n.questions.length===1?``:`s`}`,a=(0,s.renderToolHeading)(`ask`,i,t);return new s.DoomToolCall(r?`${a} ${t.fg(`dim`,`(${r})`)}`:a)},renderResult(e,t,n,r){let i=e.details;if(!i){let t=e.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
13
+ Use the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,promptSnippet:g?.promptSnippet??l,promptGuidelines:g?.promptGuidelines??u,parameters:t.QuestionParamsSchema,executionMode:`sequential`,async execute(t,o,s,c,l){let u=o,g=e=>h.isActive?.(l,e)??!0,_=()=>({answers:[],cancelled:!0});if(!g(s))return n.buildToolResult(`Error: the questionnaire session is no longer active.`,_());if(!l.hasUI)return n.buildToolResult(`Error: UI not available (running in non-interactive mode)`,{answers:[],cancelled:!0,error:`no_ui`});let v=r.validateQuestionnaire(u);if(!v.ok)return n.buildToolResult(v.message,{answers:[],cancelled:!0,error:v.error});try{let t=await h.enqueue(async({signal:e,reportProgress:t})=>{if(!g(e))return _();d(m,u);let n=await h.tryVoice?.(u);if(n)return n;let r=!0,o=()=>{r&&(r=!1,g()&&f(m,!1))};e.addEventListener(`abort`,o,{once:!0}),f(m,!0);try{if(!g(e))return _();if(l.mode===`rpc`&&p(l)){let n=await a.runRpcQuestionnaire(l,u,e,t);return g(e)?n:_()}let n=await h.runTui(l,u,i.resolveCollapseKey(i.loadUserFeedbackConfig()),e,t);if(!g(e))return _();if(n!==void 0)return n;if(p(l)){let n=await a.runRpcQuestionnaire(l,u,e,t);return g(e)?n:_()}return{answers:[],cancelled:!0,error:`no_custom_ui`}}finally{e.removeEventListener(`abort`,o),o()}},s);return t.delivery===`voice`?e.buildVoiceToolResult(t):t.error===`no_custom_ui`?n.buildToolResult(`Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.`,t):n.buildQuestionnaireResponse(t,u)}catch(e){let t=e instanceof Error?e.message:String(e);return n.buildToolResult(`Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead. (cause: ${t})`,{answers:[],cancelled:!0,error:`session_load_failed`})}},renderCall(e,t){let n=e,r=n.questions.map(e=>e.header).join(`, `),i=`${n.questions.length} question${n.questions.length===1?``:`s`}`,a=(0,s.renderToolHeading)(`ask`,i,t);return new s.DoomToolCall(r?`${a} ${t.fg(`dim`,`(${r})`)}`:a)},renderResult(e,t,n,r){let i=e.details;if(!i){let t=e.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
14
14
  `),i=r.isError?`error`:`toolOutput`,a=r.isError?`✗ failed`:`✓ submitted`;return new s.DoomToolResult([n.fg(i,t||a)],n,{wrap:!0})}if(i.cancelled)return new s.DoomToolResult([n.fg(`warning`,`◐`)+n.fg(`dim`,` cancelled`)],n);if(i.delivery===`voice`&&i.voicePrompt)return new s.DoomToolResult([n.fg(`toolOutput`,i.voicePrompt)],n,{wrap:!0});let a=i.answers.map(e=>{let t=e.kind===`multi`?(e.selected??[]).join(`, `):e.answer;return`${n.fg(`success`,`✓`)} ${n.fg(`accent`,e.question)}: ${n.fg(`toolOutput`,t??``)}`});return a.length===0&&a.push(n.fg(`success`,`✓`)+n.fg(`dim`,` submitted`)),new s.DoomToolResult(a,n,{wrap:!0})}})}exports.ASK_USER_QUESTION_TOOL_NAME=c,exports.DEFAULT_PROMPT_GUIDELINES=u,exports.DEFAULT_PROMPT_SNIPPET=l,exports.registerAskUserQuestionTool=m;
15
15
  //# sourceMappingURL=askUserQuestionAdapter.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"askUserQuestionAdapter.cjs","names":["loadUserFeedbackConfig","QuestionParamsSchema","buildToolResult","validateQuestionnaire","runRpcQuestionnaire","resolveCollapseKey","buildVoiceToolResult","buildQuestionnaireResponse","renderToolHeading","DoomToolCall","DoomToolResult"],"sources":["../../../src/adapters/pi/askUserQuestionAdapter.ts"],"sourcesContent":["import { publishAskUserBlocked, publishAskUserPrompt } from '@agimon-ai/doompi-extension-contracts';\nimport { DoomToolCall, DoomToolResult, renderToolHeading } from '@agimon-ai/doompi-ui/toolChrome';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport type { AskUserBlockedEventPayload, AskUserPromptEventPayload } from '../../schemas/events.js';\nimport {\n MAX_OPTIONS,\n MAX_QUESTIONS,\n MIN_OPTIONS,\n type QuestionParams,\n QuestionParamsSchema,\n} from '../../schemas/questionnaire.js';\nimport type { QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport { buildQuestionnaireResponse, buildToolResult } from '../../services/responseService.js';\nimport { validateQuestionnaire } from '../../services/validationService.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { loadUserFeedbackConfig, resolveCollapseKey } from '../config/configAdapter.js';\nimport { buildVoiceToolResult } from '../doom/voiceQuestionHandoff.js';\nimport { runRpcQuestionnaire } from './rpcQuestionnaireAdapter.js';\n\nexport const ASK_USER_QUESTION_TOOL_NAME = 'ask_user_question';\n\nexport const DEFAULT_PROMPT_SNIPPET = `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous`;\nexport const DEFAULT_PROMPT_GUIDELINES = [\n `Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to ${MAX_QUESTIONS} questions per invocation.`,\n `Each question MUST have ${MIN_OPTIONS}-${MAX_OPTIONS} options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended \"Type something.\" row on every question, or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.`,\n 'Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append \"(Recommended)\" to its label.',\n 'Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.',\n];\n\nconst ERROR_NO_UI = 'Error: UI not available (running in non-interactive mode)';\nconst ERROR_NO_CUSTOM_UI =\n 'Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.';\nconst ERROR_SESSION_LOAD_FAILED =\n 'Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead.';\n\nexport interface AskUserQuestionToolDependencies {\n enqueue: (runner: QuestionnaireRunner, signal?: AbortSignal) => Promise<QuestionnaireResult>;\n tryVoice?: (params: QuestionParams) => QuestionnaireResult | undefined;\n runTui: (\n context: ExtensionContext,\n params: QuestionParams,\n collapseKey: string,\n signal?: AbortSignal,\n reportProgress?: (result: QuestionnaireResult) => void,\n ) => Promise<QuestionnaireResult | undefined>;\n}\n\nfunction emitPrompt(pi: ExtensionAPI, params: QuestionParams): void {\n const payload: AskUserPromptEventPayload = {\n questions: params.questions.map((question) => ({\n question: question.question,\n header: question.header,\n multiSelect: question.multiSelect ?? false,\n options: question.options.map((option) => ({\n label: option.label,\n description: option.description,\n hasPreview: Boolean(option.preview),\n })),\n })),\n };\n publishAskUserPrompt(pi.events, payload);\n}\n\nfunction emitBlocked(pi: ExtensionAPI, active: boolean): void {\n const payload: AskUserBlockedEventPayload = { active };\n publishAskUserBlocked(pi.events, payload);\n}\n\nfunction hasDialogUI(context: ExtensionContext): boolean {\n const ui = context.ui as unknown as { select?: unknown; input?: unknown };\n return typeof ui.select === 'function' && typeof ui.input === 'function';\n}\n\nexport function registerAskUserQuestionTool(pi: ExtensionAPI, dependencies: AskUserQuestionToolDependencies): void {\n const configuredGuidance = loadUserFeedbackConfig().guidance;\n pi.registerTool({\n name: ASK_USER_QUESTION_TOOL_NAME,\n label: 'Ask User Question',\n renderShell: 'self',\n description: `Ask the user one or more structured questions during execution. Use when you need to:\n1. Gather user preferences or requirements\n2. Clarify ambiguous instructions\n3. Get decisions on implementation choices as you work\n4. Offer choices to the user about what direction to take\n\nUsage notes:\n- Users can type a custom answer via the automatically appended \"Type something.\" row on every question or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.\n- Use multiSelect: true when multiple answers are valid.\n- If you recommend a specific option, make that the first option and add \"(Recommended)\" at the end of the label.\n\nPreview feature:\nUse the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,\n promptSnippet: configuredGuidance?.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,\n promptGuidelines: configuredGuidance?.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,\n parameters: QuestionParamsSchema,\n executionMode: 'sequential',\n\n async execute(_toolCallId, params, signal, _onUpdate, context) {\n const typed = params as QuestionParams;\n if (!context.hasUI) {\n return buildToolResult(ERROR_NO_UI, { answers: [], cancelled: true, error: 'no_ui' });\n }\n\n const validation = validateQuestionnaire(typed);\n if (!validation.ok) {\n return buildToolResult(validation.message, {\n answers: [],\n cancelled: true,\n error: validation.error,\n });\n }\n\n try {\n const result = await dependencies.enqueue(async ({ signal: activeSignal, reportProgress }) => {\n emitPrompt(pi, typed);\n const voiceResult = dependencies.tryVoice?.(typed);\n if (voiceResult) return voiceResult;\n\n let blocked = true;\n const clearBlocked = (): void => {\n if (!blocked) return;\n blocked = false;\n emitBlocked(pi, false);\n };\n activeSignal.addEventListener('abort', clearBlocked, { once: true });\n emitBlocked(pi, true);\n try {\n if (context.mode === 'rpc' && hasDialogUI(context)) {\n return await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n }\n\n const tuiResult = await dependencies.runTui(\n context,\n typed,\n resolveCollapseKey(loadUserFeedbackConfig()),\n activeSignal,\n reportProgress,\n );\n if (tuiResult !== undefined) return tuiResult;\n if (hasDialogUI(context)) {\n return await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n }\n return { answers: [], cancelled: true, error: 'no_custom_ui' };\n } finally {\n activeSignal.removeEventListener('abort', clearBlocked);\n clearBlocked();\n }\n }, signal);\n\n if (result.delivery === 'voice') return buildVoiceToolResult(result);\n if (result.error === 'no_custom_ui') return buildToolResult(ERROR_NO_CUSTOM_UI, result);\n return buildQuestionnaireResponse(result, typed);\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n return buildToolResult(`${ERROR_SESSION_LOAD_FAILED} (cause: ${cause})`, {\n answers: [],\n cancelled: true,\n error: 'session_load_failed',\n });\n }\n },\n\n renderCall(args, theme) {\n const params = args as QuestionParams;\n const headers = params.questions.map((question) => question.header).join(', ');\n const count = `${params.questions.length} question${params.questions.length === 1 ? '' : 's'}`;\n const heading = renderToolHeading('ask', count, theme);\n return new DoomToolCall(headers ? `${heading} ${theme.fg('dim', `(${headers})`)}` : heading);\n },\n\n renderResult(result, _options, theme, context) {\n const details = result.details as QuestionnaireResult | undefined;\n if (!details) {\n const raw = result.content\n .filter((block) => block.type === 'text')\n .map((block) => block.text)\n .join('\\n');\n const color = context.isError ? 'error' : 'toolOutput';\n const fallback = context.isError ? '✗ failed' : '✓ submitted';\n return new DoomToolResult([theme.fg(color, raw || fallback)], theme, { wrap: true });\n }\n if (details.cancelled) {\n return new DoomToolResult([theme.fg('warning', '◐') + theme.fg('dim', ' cancelled')], theme);\n }\n if (details.delivery === 'voice' && details.voicePrompt) {\n return new DoomToolResult([theme.fg('toolOutput', details.voicePrompt)], theme, { wrap: true });\n }\n const lines = details.answers.map((answer) => {\n const scalar = answer.kind === 'multi' ? (answer.selected ?? []).join(', ') : answer.answer;\n return `${theme.fg('success', '✓')} ${theme.fg('accent', answer.question)}: ${theme.fg(\n 'toolOutput',\n scalar ?? '',\n )}`;\n });\n if (lines.length === 0) lines.push(theme.fg('success', '✓') + theme.fg('dim', ' submitted'));\n return new DoomToolResult(lines, theme, { wrap: true });\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"askUserQuestionAdapter.cjs","names":["DOOM_ASK_USER_PROMPT_EVENT","DOOM_ASK_USER_BLOCKED_EVENT","loadUserFeedbackConfig","QuestionParamsSchema","buildToolResult","validateQuestionnaire","runRpcQuestionnaire","resolveCollapseKey","buildVoiceToolResult","buildQuestionnaireResponse","renderToolHeading","DoomToolCall","DoomToolResult"],"sources":["../../../src/adapters/pi/askUserQuestionAdapter.ts"],"sourcesContent":["import {\n type AskUserBlockedEvent,\n type AskUserPromptEvent,\n DOOM_ASK_USER_BLOCKED_EVENT,\n DOOM_ASK_USER_PROMPT_EVENT,\n} from '@agimon-ai/doompi-extension-contracts/ask-user';\nimport { DoomToolCall, DoomToolResult, renderToolHeading } from '@agimon-ai/doompi-ui/toolChrome';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n MAX_OPTIONS,\n MAX_QUESTIONS,\n MIN_OPTIONS,\n type QuestionParams,\n QuestionParamsSchema,\n} from '../../schemas/questionnaire.js';\nimport type { QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport { buildQuestionnaireResponse, buildToolResult } from '../../services/responseService.js';\nimport { validateQuestionnaire } from '../../services/validationService.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { loadUserFeedbackConfig, resolveCollapseKey } from '../config/configAdapter.js';\nimport { buildVoiceToolResult } from '../doom/voiceQuestionHandoff.js';\nimport { runRpcQuestionnaire } from './rpcQuestionnaireAdapter.js';\n\nexport const ASK_USER_QUESTION_TOOL_NAME = 'ask_user_question';\n\nexport const DEFAULT_PROMPT_SNIPPET = `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous`;\nexport const DEFAULT_PROMPT_GUIDELINES = [\n `Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to ${MAX_QUESTIONS} questions per invocation.`,\n `Each question MUST have ${MIN_OPTIONS}-${MAX_OPTIONS} options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended \"Type something.\" row on every question, or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.`,\n 'Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append \"(Recommended)\" to its label.',\n 'Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.',\n];\n\nconst ERROR_NO_UI = 'Error: UI not available (running in non-interactive mode)';\nconst ERROR_NO_CUSTOM_UI =\n 'Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.';\nconst ERROR_SESSION_LOAD_FAILED =\n 'Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead.';\nconst ERROR_SESSION_INACTIVE = 'Error: the questionnaire session is no longer active.';\n\nexport interface AskUserQuestionToolDependencies {\n enqueue: (runner: QuestionnaireRunner, signal?: AbortSignal) => Promise<QuestionnaireResult>;\n isActive?: (context: ExtensionContext, signal?: AbortSignal) => boolean;\n tryVoice?: (params: QuestionParams) => QuestionnaireResult | undefined | Promise<QuestionnaireResult | undefined>;\n runTui: (\n context: ExtensionContext,\n params: QuestionParams,\n collapseKey: string,\n signal?: AbortSignal,\n reportProgress?: (result: QuestionnaireResult) => void,\n ) => Promise<QuestionnaireResult | undefined>;\n}\n\nfunction emitPrompt(cordis: Context, params: QuestionParams): void {\n const payload: AskUserPromptEvent = {\n questions: params.questions.map((question) => ({\n question: question.question,\n header: question.header,\n multiSelect: question.multiSelect ?? false,\n options: question.options.map((option) => ({\n label: option.label,\n description: option.description,\n hasPreview: Boolean(option.preview),\n })),\n })),\n };\n cordis.emit(DOOM_ASK_USER_PROMPT_EVENT, payload);\n}\n\nfunction emitBlocked(cordis: Context, active: boolean): void {\n const payload: AskUserBlockedEvent = { active };\n cordis.emit(DOOM_ASK_USER_BLOCKED_EVENT, payload);\n}\n\nfunction hasDialogUI(context: ExtensionContext): boolean {\n const ui = context.ui as unknown as { select?: unknown; input?: unknown };\n return typeof ui.select === 'function' && typeof ui.input === 'function';\n}\n\nexport function registerAskUserQuestionTool(\n pi: ExtensionAPI,\n cordis: Context,\n dependencies: AskUserQuestionToolDependencies,\n): void {\n const configuredGuidance = loadUserFeedbackConfig().guidance;\n pi.registerTool({\n name: ASK_USER_QUESTION_TOOL_NAME,\n label: 'Ask User Question',\n renderShell: 'self',\n description: `Ask the user one or more structured questions during execution. Use when you need to:\n1. Gather user preferences or requirements\n2. Clarify ambiguous instructions\n3. Get decisions on implementation choices as you work\n4. Offer choices to the user about what direction to take\n\nUsage notes:\n- Users can type a custom answer via the automatically appended \"Type something.\" row on every question or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.\n- Use multiSelect: true when multiple answers are valid.\n- If you recommend a specific option, make that the first option and add \"(Recommended)\" at the end of the label.\n\nPreview feature:\nUse the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,\n promptSnippet: configuredGuidance?.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,\n promptGuidelines: configuredGuidance?.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,\n parameters: QuestionParamsSchema,\n executionMode: 'sequential',\n\n async execute(_toolCallId, params, signal, _onUpdate, context) {\n const typed = params as QuestionParams;\n const isActive = (activeSignal?: AbortSignal): boolean => dependencies.isActive?.(context, activeSignal) ?? true;\n const cancelled = (): QuestionnaireResult => ({ answers: [], cancelled: true });\n if (!isActive(signal)) return buildToolResult(ERROR_SESSION_INACTIVE, cancelled());\n if (!context.hasUI) {\n return buildToolResult(ERROR_NO_UI, { answers: [], cancelled: true, error: 'no_ui' });\n }\n\n const validation = validateQuestionnaire(typed);\n if (!validation.ok) {\n return buildToolResult(validation.message, {\n answers: [],\n cancelled: true,\n error: validation.error,\n });\n }\n\n try {\n const result = await dependencies.enqueue(async ({ signal: activeSignal, reportProgress }) => {\n if (!isActive(activeSignal)) return cancelled();\n emitPrompt(cordis, typed);\n const voiceResult = await dependencies.tryVoice?.(typed);\n if (voiceResult) return voiceResult;\n\n let blocked = true;\n const clearBlocked = (): void => {\n if (!blocked) return;\n blocked = false;\n if (isActive()) emitBlocked(cordis, false);\n };\n activeSignal.addEventListener('abort', clearBlocked, { once: true });\n emitBlocked(cordis, true);\n try {\n if (!isActive(activeSignal)) return cancelled();\n if (context.mode === 'rpc' && hasDialogUI(context)) {\n const rpcResult = await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n return isActive(activeSignal) ? rpcResult : cancelled();\n }\n\n const tuiResult = await dependencies.runTui(\n context,\n typed,\n resolveCollapseKey(loadUserFeedbackConfig()),\n activeSignal,\n reportProgress,\n );\n if (!isActive(activeSignal)) return cancelled();\n if (tuiResult !== undefined) return tuiResult;\n if (hasDialogUI(context)) {\n const rpcResult = await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n return isActive(activeSignal) ? rpcResult : cancelled();\n }\n return { answers: [], cancelled: true, error: 'no_custom_ui' };\n } finally {\n activeSignal.removeEventListener('abort', clearBlocked);\n clearBlocked();\n }\n }, signal);\n\n if (result.delivery === 'voice') return buildVoiceToolResult(result);\n if (result.error === 'no_custom_ui') return buildToolResult(ERROR_NO_CUSTOM_UI, result);\n return buildQuestionnaireResponse(result, typed);\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n return buildToolResult(`${ERROR_SESSION_LOAD_FAILED} (cause: ${cause})`, {\n answers: [],\n cancelled: true,\n error: 'session_load_failed',\n });\n }\n },\n\n renderCall(args, theme) {\n const params = args as QuestionParams;\n const headers = params.questions.map((question) => question.header).join(', ');\n const count = `${params.questions.length} question${params.questions.length === 1 ? '' : 's'}`;\n const heading = renderToolHeading('ask', count, theme);\n return new DoomToolCall(headers ? `${heading} ${theme.fg('dim', `(${headers})`)}` : heading);\n },\n\n renderResult(result, _options, theme, context) {\n const details = result.details as QuestionnaireResult | undefined;\n if (!details) {\n const raw = result.content\n .filter((block) => block.type === 'text')\n .map((block) => block.text)\n .join('\\n');\n const color = context.isError ? 'error' : 'toolOutput';\n const fallback = context.isError ? '✗ failed' : '✓ submitted';\n return new DoomToolResult([theme.fg(color, raw || fallback)], theme, { wrap: true });\n }\n if (details.cancelled) {\n return new DoomToolResult([theme.fg('warning', '◐') + theme.fg('dim', ' cancelled')], theme);\n }\n if (details.delivery === 'voice' && details.voicePrompt) {\n return new DoomToolResult([theme.fg('toolOutput', details.voicePrompt)], theme, { wrap: true });\n }\n const lines = details.answers.map((answer) => {\n const scalar = answer.kind === 'multi' ? (answer.selected ?? []).join(', ') : answer.answer;\n return `${theme.fg('success', '✓')} ${theme.fg('accent', answer.question)}: ${theme.fg(\n 'toolOutput',\n scalar ?? '',\n )}`;\n });\n if (lines.length === 0) lines.push(theme.fg('success', '✓') + theme.fg('dim', ' submitted'));\n return new DoomToolResult(lines, theme, { wrap: true });\n },\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import{buildVoiceToolResult as e}from"../doom/voiceQuestionHandoff.mjs";import{QuestionParamsSchema as t}from"../../schemas/questionnaire.mjs";import{buildQuestionnaireResponse as n,buildToolResult as r}from"../../services/responseService.mjs";import{validateQuestionnaire as i}from"../../services/validationService.mjs";import{loadUserFeedbackConfig as a,resolveCollapseKey as o}from"../config/configAdapter.mjs";import{runRpcQuestionnaire as s}from"./rpcQuestionnaireAdapter.mjs";import{publishAskUserBlocked as c,publishAskUserPrompt as l}from"@agimon-ai/doompi-extension-contracts";import{DoomToolCall as u,DoomToolResult as d,renderToolHeading as f}from"@agimon-ai/doompi-ui/toolChrome";const p=`ask_user_question`,m=`Ask the user up to 4 structured questions (2-4 options each) when requirements are ambiguous`,h=[`Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to 4 questions per invocation.`,`Each question MUST have 2-4 options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended "Type something." row on every question, or press Esc to abandon the questionnaire. Do NOT author "Other" or "Type something." labels yourself — reserved labels are rejected at runtime.`,`Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append "(Recommended)" to its label.`,`Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.`];function g(e,t){let n={questions:t.questions.map(e=>({question:e.question,header:e.header,multiSelect:e.multiSelect??!1,options:e.options.map(e=>({label:e.label,description:e.description,hasPreview:!!e.preview}))}))};l(e.events,n)}function _(e,t){let n={active:t};c(e.events,n)}function v(e){let t=e.ui;return typeof t.select==`function`&&typeof t.input==`function`}function y(c,l){let y=a().guidance;c.registerTool({name:p,label:`Ask User Question`,renderShell:`self`,description:`Ask the user one or more structured questions during execution. Use when you need to:
1
+ import{buildVoiceToolResult as e}from"../doom/voiceQuestionHandoff.mjs";import{QuestionParamsSchema as t}from"../../schemas/questionnaire.mjs";import{buildQuestionnaireResponse as n,buildToolResult as r}from"../../services/responseService.mjs";import{validateQuestionnaire as i}from"../../services/validationService.mjs";import{loadUserFeedbackConfig as a,resolveCollapseKey as o}from"../config/configAdapter.mjs";import{runRpcQuestionnaire as s}from"./rpcQuestionnaireAdapter.mjs";import{DOOM_ASK_USER_BLOCKED_EVENT as c,DOOM_ASK_USER_PROMPT_EVENT as l}from"@agimon-ai/doompi-extension-contracts/ask-user";import{DoomToolCall as u,DoomToolResult as d,renderToolHeading as f}from"@agimon-ai/doompi-ui/toolChrome";const p=`ask_user_question`,m=`Ask the user up to 4 structured questions (2-4 options each) when requirements are ambiguous`,h=[`Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to 4 questions per invocation.`,`Each question MUST have 2-4 options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended "Type something." row on every question, or press Esc to abandon the questionnaire. Do NOT author "Other" or "Type something." labels yourself — reserved labels are rejected at runtime.`,`Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append "(Recommended)" to its label.`,`Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.`];function g(e,t){let n={questions:t.questions.map(e=>({question:e.question,header:e.header,multiSelect:e.multiSelect??!1,options:e.options.map(e=>({label:e.label,description:e.description,hasPreview:!!e.preview}))}))};e.emit(l,n)}function _(e,t){let n={active:t};e.emit(c,n)}function v(e){let t=e.ui;return typeof t.select==`function`&&typeof t.input==`function`}function y(c,l,y){let b=a().guidance;c.registerTool({name:p,label:`Ask User Question`,renderShell:`self`,description:`Ask the user one or more structured questions during execution. Use when you need to:
2
2
  1. Gather user preferences or requirements
3
3
  2. Clarify ambiguous instructions
4
4
  3. Get decisions on implementation choices as you work
@@ -10,6 +10,6 @@ Usage notes:
10
10
  - If you recommend a specific option, make that the first option and add "(Recommended)" at the end of the label.
11
11
 
12
12
  Preview feature:
13
- Use the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,promptSnippet:y?.promptSnippet??m,promptGuidelines:y?.promptGuidelines??h,parameters:t,executionMode:`sequential`,async execute(t,u,d,f,p){let m=u;if(!p.hasUI)return r(`Error: UI not available (running in non-interactive mode)`,{answers:[],cancelled:!0,error:`no_ui`});let h=i(m);if(!h.ok)return r(h.message,{answers:[],cancelled:!0,error:h.error});try{let t=await l.enqueue(async({signal:e,reportProgress:t})=>{g(c,m);let n=l.tryVoice?.(m);if(n)return n;let r=!0,i=()=>{r&&(r=!1,_(c,!1))};e.addEventListener(`abort`,i,{once:!0}),_(c,!0);try{if(p.mode===`rpc`&&v(p))return await s(p,m,e,t);let n=await l.runTui(p,m,o(a()),e,t);return n===void 0?v(p)?await s(p,m,e,t):{answers:[],cancelled:!0,error:`no_custom_ui`}:n}finally{e.removeEventListener(`abort`,i),i()}},d);return t.delivery===`voice`?e(t):t.error===`no_custom_ui`?r(`Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.`,t):n(t,m)}catch(e){let t=e instanceof Error?e.message:String(e);return r(`Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead. (cause: ${t})`,{answers:[],cancelled:!0,error:`session_load_failed`})}},renderCall(e,t){let n=e,r=n.questions.map(e=>e.header).join(`, `),i=`${n.questions.length} question${n.questions.length===1?``:`s`}`,a=f(`ask`,i,t);return new u(r?`${a} ${t.fg(`dim`,`(${r})`)}`:a)},renderResult(e,t,n,r){let i=e.details;if(!i){let t=e.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
13
+ Use the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,promptSnippet:b?.promptSnippet??m,promptGuidelines:b?.promptGuidelines??h,parameters:t,executionMode:`sequential`,async execute(t,c,u,d,f){let p=c,m=e=>y.isActive?.(f,e)??!0,h=()=>({answers:[],cancelled:!0});if(!m(u))return r(`Error: the questionnaire session is no longer active.`,h());if(!f.hasUI)return r(`Error: UI not available (running in non-interactive mode)`,{answers:[],cancelled:!0,error:`no_ui`});let b=i(p);if(!b.ok)return r(b.message,{answers:[],cancelled:!0,error:b.error});try{let t=await y.enqueue(async({signal:e,reportProgress:t})=>{if(!m(e))return h();g(l,p);let n=await y.tryVoice?.(p);if(n)return n;let r=!0,i=()=>{r&&(r=!1,m()&&_(l,!1))};e.addEventListener(`abort`,i,{once:!0}),_(l,!0);try{if(!m(e))return h();if(f.mode===`rpc`&&v(f)){let n=await s(f,p,e,t);return m(e)?n:h()}let n=await y.runTui(f,p,o(a()),e,t);if(!m(e))return h();if(n!==void 0)return n;if(v(f)){let n=await s(f,p,e,t);return m(e)?n:h()}return{answers:[],cancelled:!0,error:`no_custom_ui`}}finally{e.removeEventListener(`abort`,i),i()}},u);return t.delivery===`voice`?e(t):t.error===`no_custom_ui`?r(`Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.`,t):n(t,p)}catch(e){let t=e instanceof Error?e.message:String(e);return r(`Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead. (cause: ${t})`,{answers:[],cancelled:!0,error:`session_load_failed`})}},renderCall(e,t){let n=e,r=n.questions.map(e=>e.header).join(`, `),i=`${n.questions.length} question${n.questions.length===1?``:`s`}`,a=f(`ask`,i,t);return new u(r?`${a} ${t.fg(`dim`,`(${r})`)}`:a)},renderResult(e,t,n,r){let i=e.details;if(!i){let t=e.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
14
14
  `),i=r.isError?`error`:`toolOutput`,a=r.isError?`✗ failed`:`✓ submitted`;return new d([n.fg(i,t||a)],n,{wrap:!0})}if(i.cancelled)return new d([n.fg(`warning`,`◐`)+n.fg(`dim`,` cancelled`)],n);if(i.delivery===`voice`&&i.voicePrompt)return new d([n.fg(`toolOutput`,i.voicePrompt)],n,{wrap:!0});let a=i.answers.map(e=>{let t=e.kind===`multi`?(e.selected??[]).join(`, `):e.answer;return`${n.fg(`success`,`✓`)} ${n.fg(`accent`,e.question)}: ${n.fg(`toolOutput`,t??``)}`});return a.length===0&&a.push(n.fg(`success`,`✓`)+n.fg(`dim`,` submitted`)),new d(a,n,{wrap:!0})}})}export{p as ASK_USER_QUESTION_TOOL_NAME,h as DEFAULT_PROMPT_GUIDELINES,m as DEFAULT_PROMPT_SNIPPET,y as registerAskUserQuestionTool};
15
15
  //# sourceMappingURL=askUserQuestionAdapter.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"askUserQuestionAdapter.mjs","names":[],"sources":["../../../src/adapters/pi/askUserQuestionAdapter.ts"],"sourcesContent":["import { publishAskUserBlocked, publishAskUserPrompt } from '@agimon-ai/doompi-extension-contracts';\nimport { DoomToolCall, DoomToolResult, renderToolHeading } from '@agimon-ai/doompi-ui/toolChrome';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport type { AskUserBlockedEventPayload, AskUserPromptEventPayload } from '../../schemas/events.js';\nimport {\n MAX_OPTIONS,\n MAX_QUESTIONS,\n MIN_OPTIONS,\n type QuestionParams,\n QuestionParamsSchema,\n} from '../../schemas/questionnaire.js';\nimport type { QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport { buildQuestionnaireResponse, buildToolResult } from '../../services/responseService.js';\nimport { validateQuestionnaire } from '../../services/validationService.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { loadUserFeedbackConfig, resolveCollapseKey } from '../config/configAdapter.js';\nimport { buildVoiceToolResult } from '../doom/voiceQuestionHandoff.js';\nimport { runRpcQuestionnaire } from './rpcQuestionnaireAdapter.js';\n\nexport const ASK_USER_QUESTION_TOOL_NAME = 'ask_user_question';\n\nexport const DEFAULT_PROMPT_SNIPPET = `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous`;\nexport const DEFAULT_PROMPT_GUIDELINES = [\n `Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to ${MAX_QUESTIONS} questions per invocation.`,\n `Each question MUST have ${MIN_OPTIONS}-${MAX_OPTIONS} options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended \"Type something.\" row on every question, or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.`,\n 'Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append \"(Recommended)\" to its label.',\n 'Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.',\n];\n\nconst ERROR_NO_UI = 'Error: UI not available (running in non-interactive mode)';\nconst ERROR_NO_CUSTOM_UI =\n 'Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.';\nconst ERROR_SESSION_LOAD_FAILED =\n 'Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead.';\n\nexport interface AskUserQuestionToolDependencies {\n enqueue: (runner: QuestionnaireRunner, signal?: AbortSignal) => Promise<QuestionnaireResult>;\n tryVoice?: (params: QuestionParams) => QuestionnaireResult | undefined;\n runTui: (\n context: ExtensionContext,\n params: QuestionParams,\n collapseKey: string,\n signal?: AbortSignal,\n reportProgress?: (result: QuestionnaireResult) => void,\n ) => Promise<QuestionnaireResult | undefined>;\n}\n\nfunction emitPrompt(pi: ExtensionAPI, params: QuestionParams): void {\n const payload: AskUserPromptEventPayload = {\n questions: params.questions.map((question) => ({\n question: question.question,\n header: question.header,\n multiSelect: question.multiSelect ?? false,\n options: question.options.map((option) => ({\n label: option.label,\n description: option.description,\n hasPreview: Boolean(option.preview),\n })),\n })),\n };\n publishAskUserPrompt(pi.events, payload);\n}\n\nfunction emitBlocked(pi: ExtensionAPI, active: boolean): void {\n const payload: AskUserBlockedEventPayload = { active };\n publishAskUserBlocked(pi.events, payload);\n}\n\nfunction hasDialogUI(context: ExtensionContext): boolean {\n const ui = context.ui as unknown as { select?: unknown; input?: unknown };\n return typeof ui.select === 'function' && typeof ui.input === 'function';\n}\n\nexport function registerAskUserQuestionTool(pi: ExtensionAPI, dependencies: AskUserQuestionToolDependencies): void {\n const configuredGuidance = loadUserFeedbackConfig().guidance;\n pi.registerTool({\n name: ASK_USER_QUESTION_TOOL_NAME,\n label: 'Ask User Question',\n renderShell: 'self',\n description: `Ask the user one or more structured questions during execution. Use when you need to:\n1. Gather user preferences or requirements\n2. Clarify ambiguous instructions\n3. Get decisions on implementation choices as you work\n4. Offer choices to the user about what direction to take\n\nUsage notes:\n- Users can type a custom answer via the automatically appended \"Type something.\" row on every question or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.\n- Use multiSelect: true when multiple answers are valid.\n- If you recommend a specific option, make that the first option and add \"(Recommended)\" at the end of the label.\n\nPreview feature:\nUse the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,\n promptSnippet: configuredGuidance?.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,\n promptGuidelines: configuredGuidance?.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,\n parameters: QuestionParamsSchema,\n executionMode: 'sequential',\n\n async execute(_toolCallId, params, signal, _onUpdate, context) {\n const typed = params as QuestionParams;\n if (!context.hasUI) {\n return buildToolResult(ERROR_NO_UI, { answers: [], cancelled: true, error: 'no_ui' });\n }\n\n const validation = validateQuestionnaire(typed);\n if (!validation.ok) {\n return buildToolResult(validation.message, {\n answers: [],\n cancelled: true,\n error: validation.error,\n });\n }\n\n try {\n const result = await dependencies.enqueue(async ({ signal: activeSignal, reportProgress }) => {\n emitPrompt(pi, typed);\n const voiceResult = dependencies.tryVoice?.(typed);\n if (voiceResult) return voiceResult;\n\n let blocked = true;\n const clearBlocked = (): void => {\n if (!blocked) return;\n blocked = false;\n emitBlocked(pi, false);\n };\n activeSignal.addEventListener('abort', clearBlocked, { once: true });\n emitBlocked(pi, true);\n try {\n if (context.mode === 'rpc' && hasDialogUI(context)) {\n return await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n }\n\n const tuiResult = await dependencies.runTui(\n context,\n typed,\n resolveCollapseKey(loadUserFeedbackConfig()),\n activeSignal,\n reportProgress,\n );\n if (tuiResult !== undefined) return tuiResult;\n if (hasDialogUI(context)) {\n return await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n }\n return { answers: [], cancelled: true, error: 'no_custom_ui' };\n } finally {\n activeSignal.removeEventListener('abort', clearBlocked);\n clearBlocked();\n }\n }, signal);\n\n if (result.delivery === 'voice') return buildVoiceToolResult(result);\n if (result.error === 'no_custom_ui') return buildToolResult(ERROR_NO_CUSTOM_UI, result);\n return buildQuestionnaireResponse(result, typed);\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n return buildToolResult(`${ERROR_SESSION_LOAD_FAILED} (cause: ${cause})`, {\n answers: [],\n cancelled: true,\n error: 'session_load_failed',\n });\n }\n },\n\n renderCall(args, theme) {\n const params = args as QuestionParams;\n const headers = params.questions.map((question) => question.header).join(', ');\n const count = `${params.questions.length} question${params.questions.length === 1 ? '' : 's'}`;\n const heading = renderToolHeading('ask', count, theme);\n return new DoomToolCall(headers ? `${heading} ${theme.fg('dim', `(${headers})`)}` : heading);\n },\n\n renderResult(result, _options, theme, context) {\n const details = result.details as QuestionnaireResult | undefined;\n if (!details) {\n const raw = result.content\n .filter((block) => block.type === 'text')\n .map((block) => block.text)\n .join('\\n');\n const color = context.isError ? 'error' : 'toolOutput';\n const fallback = context.isError ? '✗ failed' : '✓ submitted';\n return new DoomToolResult([theme.fg(color, raw || fallback)], theme, { wrap: true });\n }\n if (details.cancelled) {\n return new DoomToolResult([theme.fg('warning', '◐') + theme.fg('dim', ' cancelled')], theme);\n }\n if (details.delivery === 'voice' && details.voicePrompt) {\n return new DoomToolResult([theme.fg('toolOutput', details.voicePrompt)], theme, { wrap: true });\n }\n const lines = details.answers.map((answer) => {\n const scalar = answer.kind === 'multi' ? (answer.selected ?? []).join(', ') : answer.answer;\n return `${theme.fg('success', '✓')} ${theme.fg('accent', answer.question)}: ${theme.fg(\n 'toolOutput',\n scalar ?? '',\n )}`;\n });\n if (lines.length === 0) lines.push(theme.fg('success', '✓') + theme.fg('dim', ' submitted'));\n return new DoomToolResult(lines, theme, { wrap: true });\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"askUserQuestionAdapter.mjs","names":[],"sources":["../../../src/adapters/pi/askUserQuestionAdapter.ts"],"sourcesContent":["import {\n type AskUserBlockedEvent,\n type AskUserPromptEvent,\n DOOM_ASK_USER_BLOCKED_EVENT,\n DOOM_ASK_USER_PROMPT_EVENT,\n} from '@agimon-ai/doompi-extension-contracts/ask-user';\nimport { DoomToolCall, DoomToolResult, renderToolHeading } from '@agimon-ai/doompi-ui/toolChrome';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n MAX_OPTIONS,\n MAX_QUESTIONS,\n MIN_OPTIONS,\n type QuestionParams,\n QuestionParamsSchema,\n} from '../../schemas/questionnaire.js';\nimport type { QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport { buildQuestionnaireResponse, buildToolResult } from '../../services/responseService.js';\nimport { validateQuestionnaire } from '../../services/validationService.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { loadUserFeedbackConfig, resolveCollapseKey } from '../config/configAdapter.js';\nimport { buildVoiceToolResult } from '../doom/voiceQuestionHandoff.js';\nimport { runRpcQuestionnaire } from './rpcQuestionnaireAdapter.js';\n\nexport const ASK_USER_QUESTION_TOOL_NAME = 'ask_user_question';\n\nexport const DEFAULT_PROMPT_SNIPPET = `Ask the user up to ${MAX_QUESTIONS} structured questions (${MIN_OPTIONS}-${MAX_OPTIONS} options each) when requirements are ambiguous`;\nexport const DEFAULT_PROMPT_GUIDELINES = [\n `Use ask_user_question whenever the user's request is underspecified and you cannot proceed without concrete decisions — you can ask up to ${MAX_QUESTIONS} questions per invocation.`,\n `Each question MUST have ${MIN_OPTIONS}-${MAX_OPTIONS} options. Every option requires a concise label (1-5 words) and a description explaining what the choice means or its trade-offs. The user can additionally type a custom answer via the automatically appended \"Type something.\" row on every question, or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.`,\n 'Set multiSelect: true when multiple answers are valid. Provide an options[].preview markdown string when an option benefits from richer side-by-side context (mockups, code snippets, diagrams, configs) — single-select only. If you recommend a specific option, make that the first option and append \"(Recommended)\" to its label.',\n 'Do not stack multiple ask_user_question calls back-to-back — group all clarifying questions into one invocation.',\n];\n\nconst ERROR_NO_UI = 'Error: UI not available (running in non-interactive mode)';\nconst ERROR_NO_CUSTOM_UI =\n 'Error: this client cannot render the questionnaire (custom UI is unavailable). The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead, without using this tool.';\nconst ERROR_SESSION_LOAD_FAILED =\n 'Error: the questionnaire UI failed to load. The user never saw the questions — do NOT treat this as a decline. Ask the questions as plain chat text instead.';\nconst ERROR_SESSION_INACTIVE = 'Error: the questionnaire session is no longer active.';\n\nexport interface AskUserQuestionToolDependencies {\n enqueue: (runner: QuestionnaireRunner, signal?: AbortSignal) => Promise<QuestionnaireResult>;\n isActive?: (context: ExtensionContext, signal?: AbortSignal) => boolean;\n tryVoice?: (params: QuestionParams) => QuestionnaireResult | undefined | Promise<QuestionnaireResult | undefined>;\n runTui: (\n context: ExtensionContext,\n params: QuestionParams,\n collapseKey: string,\n signal?: AbortSignal,\n reportProgress?: (result: QuestionnaireResult) => void,\n ) => Promise<QuestionnaireResult | undefined>;\n}\n\nfunction emitPrompt(cordis: Context, params: QuestionParams): void {\n const payload: AskUserPromptEvent = {\n questions: params.questions.map((question) => ({\n question: question.question,\n header: question.header,\n multiSelect: question.multiSelect ?? false,\n options: question.options.map((option) => ({\n label: option.label,\n description: option.description,\n hasPreview: Boolean(option.preview),\n })),\n })),\n };\n cordis.emit(DOOM_ASK_USER_PROMPT_EVENT, payload);\n}\n\nfunction emitBlocked(cordis: Context, active: boolean): void {\n const payload: AskUserBlockedEvent = { active };\n cordis.emit(DOOM_ASK_USER_BLOCKED_EVENT, payload);\n}\n\nfunction hasDialogUI(context: ExtensionContext): boolean {\n const ui = context.ui as unknown as { select?: unknown; input?: unknown };\n return typeof ui.select === 'function' && typeof ui.input === 'function';\n}\n\nexport function registerAskUserQuestionTool(\n pi: ExtensionAPI,\n cordis: Context,\n dependencies: AskUserQuestionToolDependencies,\n): void {\n const configuredGuidance = loadUserFeedbackConfig().guidance;\n pi.registerTool({\n name: ASK_USER_QUESTION_TOOL_NAME,\n label: 'Ask User Question',\n renderShell: 'self',\n description: `Ask the user one or more structured questions during execution. Use when you need to:\n1. Gather user preferences or requirements\n2. Clarify ambiguous instructions\n3. Get decisions on implementation choices as you work\n4. Offer choices to the user about what direction to take\n\nUsage notes:\n- Users can type a custom answer via the automatically appended \"Type something.\" row on every question or press Esc to abandon the questionnaire. Do NOT author \"Other\" or \"Type something.\" labels yourself — reserved labels are rejected at runtime.\n- Use multiSelect: true when multiple answers are valid.\n- If you recommend a specific option, make that the first option and add \"(Recommended)\" at the end of the label.\n\nPreview feature:\nUse the optional preview field for ASCII mockups, code snippets, diagrams, or configuration examples that users should compare visually. Do not use previews for simple preference questions.`,\n promptSnippet: configuredGuidance?.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,\n promptGuidelines: configuredGuidance?.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,\n parameters: QuestionParamsSchema,\n executionMode: 'sequential',\n\n async execute(_toolCallId, params, signal, _onUpdate, context) {\n const typed = params as QuestionParams;\n const isActive = (activeSignal?: AbortSignal): boolean => dependencies.isActive?.(context, activeSignal) ?? true;\n const cancelled = (): QuestionnaireResult => ({ answers: [], cancelled: true });\n if (!isActive(signal)) return buildToolResult(ERROR_SESSION_INACTIVE, cancelled());\n if (!context.hasUI) {\n return buildToolResult(ERROR_NO_UI, { answers: [], cancelled: true, error: 'no_ui' });\n }\n\n const validation = validateQuestionnaire(typed);\n if (!validation.ok) {\n return buildToolResult(validation.message, {\n answers: [],\n cancelled: true,\n error: validation.error,\n });\n }\n\n try {\n const result = await dependencies.enqueue(async ({ signal: activeSignal, reportProgress }) => {\n if (!isActive(activeSignal)) return cancelled();\n emitPrompt(cordis, typed);\n const voiceResult = await dependencies.tryVoice?.(typed);\n if (voiceResult) return voiceResult;\n\n let blocked = true;\n const clearBlocked = (): void => {\n if (!blocked) return;\n blocked = false;\n if (isActive()) emitBlocked(cordis, false);\n };\n activeSignal.addEventListener('abort', clearBlocked, { once: true });\n emitBlocked(cordis, true);\n try {\n if (!isActive(activeSignal)) return cancelled();\n if (context.mode === 'rpc' && hasDialogUI(context)) {\n const rpcResult = await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n return isActive(activeSignal) ? rpcResult : cancelled();\n }\n\n const tuiResult = await dependencies.runTui(\n context,\n typed,\n resolveCollapseKey(loadUserFeedbackConfig()),\n activeSignal,\n reportProgress,\n );\n if (!isActive(activeSignal)) return cancelled();\n if (tuiResult !== undefined) return tuiResult;\n if (hasDialogUI(context)) {\n const rpcResult = await runRpcQuestionnaire(context, typed, activeSignal, reportProgress);\n return isActive(activeSignal) ? rpcResult : cancelled();\n }\n return { answers: [], cancelled: true, error: 'no_custom_ui' };\n } finally {\n activeSignal.removeEventListener('abort', clearBlocked);\n clearBlocked();\n }\n }, signal);\n\n if (result.delivery === 'voice') return buildVoiceToolResult(result);\n if (result.error === 'no_custom_ui') return buildToolResult(ERROR_NO_CUSTOM_UI, result);\n return buildQuestionnaireResponse(result, typed);\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n return buildToolResult(`${ERROR_SESSION_LOAD_FAILED} (cause: ${cause})`, {\n answers: [],\n cancelled: true,\n error: 'session_load_failed',\n });\n }\n },\n\n renderCall(args, theme) {\n const params = args as QuestionParams;\n const headers = params.questions.map((question) => question.header).join(', ');\n const count = `${params.questions.length} question${params.questions.length === 1 ? '' : 's'}`;\n const heading = renderToolHeading('ask', count, theme);\n return new DoomToolCall(headers ? `${heading} ${theme.fg('dim', `(${headers})`)}` : heading);\n },\n\n renderResult(result, _options, theme, context) {\n const details = result.details as QuestionnaireResult | undefined;\n if (!details) {\n const raw = result.content\n .filter((block) => block.type === 'text')\n .map((block) => block.text)\n .join('\\n');\n const color = context.isError ? 'error' : 'toolOutput';\n const fallback = context.isError ? '✗ failed' : '✓ submitted';\n return new DoomToolResult([theme.fg(color, raw || fallback)], theme, { wrap: true });\n }\n if (details.cancelled) {\n return new DoomToolResult([theme.fg('warning', '◐') + theme.fg('dim', ' cancelled')], theme);\n }\n if (details.delivery === 'voice' && details.voicePrompt) {\n return new DoomToolResult([theme.fg('toolOutput', details.voicePrompt)], theme, { wrap: true });\n }\n const lines = details.answers.map((answer) => {\n const scalar = answer.kind === 'multi' ? (answer.selected ?? []).join(', ') : answer.answer;\n return `${theme.fg('success', '✓')} ${theme.fg('accent', answer.question)}: ${theme.fg(\n 'toolOutput',\n scalar ?? '',\n )}`;\n });\n if (lines.length === 0) lines.push(theme.fg('success', '✓') + theme.fg('dim', ' submitted'));\n return new DoomToolResult(lines, theme, { wrap: true });\n },\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../services/questionnaireCoordinator.cjs"),t=require("../doom/voiceQuestionHandoff.cjs"),n=require("./askUserQuestionAdapter.cjs"),r=require("./reconcileAdapter.cjs");let i=require("@agimon-ai/doompi-extension-contracts/cordis-host"),a=require("@agimon-ai/doompi-extension-contracts/mode"),o=require("@agimon-ai/doompi-extension-contracts/narration");const s=`@agimon-ai/doompi-user-feedback`;function c(){return{answers:[],cancelled:!0}}function l(i,l){let u=!0,d=0,f,p=!1,m=new e.QuestionnaireCoordinator,h,g=Promise.resolve(),_=new Set,v=e=>(_.add(e),e.then(()=>_.delete(e),()=>_.delete(e)),e),y=(e,t)=>u&&e===d&&t===f,b=(e,t)=>u&&p&&t?.aborted!==!0&&e.sessionManager.getSessionId()===f,x=async()=>{if(!u)return;let e=m;e.shutdown(),u=!1,d+=1,p=!1,f=void 0,await Promise.allSettled(_),await e.waitForIdle()};i.effect(()=>x,`${s}/runtime`),i.inject([a.DOOM_MINOR_MODE_CATALOG_SERVICE,o.DOOM_NARRATION_SERVICE],e=>{let n=t.createVoiceQuestionHandoff((0,a.requireMinorModeCatalog)(e),(0,o.requireDoomNarrationService)(e));return h=n,()=>{h===n&&(h=void 0)}}),n.registerAskUserQuestionTool(l,i,{enqueue:(e,t)=>{let n=m,r=d,i=f;return!p||!y(r,i)||t?.aborted?Promise.resolve(c()):n.enqueue(async t=>{if(!p||!y(r,i)||t.signal.aborted)return c();let n=await e({...t,reportProgress:e=>{y(r,i)&&!t.signal.aborted&&t.reportProgress(e)}});return p&&y(r,i)&&!t.signal.aborted?n:c()},t)},isActive:b,runTui:async(e,t,n,r,i)=>{if(!b(e,r))return;let{runTuiQuestionnaire:a}=await Promise.resolve().then(()=>require("../../tui/runQuestionnaire.cjs"));if(!b(e,r))return;let o=await a(e,t,n,r,i);return b(e,r)?o:void 0},tryVoice:e=>u&&p?h?.handoff(e):void 0}),r.registerAskUserQuestionReconciler(l,b),l.on(`session_start`,(t,n)=>{if(!u)return;let r=m;r.shutdown();let i=n.sessionManager.getSessionId(),a=++d;m=new e.QuestionnaireCoordinator,f=i,p=!1;let o=async()=>{await r.waitForIdle(),y(a,i)&&(p=!0)},s=g.then(o,o);return g=s.catch(()=>void 0),v(s)})}async function u(e){let t=await(0,i.connectDoomCordisHost)(e,s),n=t.root.plugin(d,{pi:e});try{await n}catch(e){try{await n.dispose()}finally{await t.dispose()}throw e}let r;e.on(`session_shutdown`,()=>r??=(async()=>{try{await n.dispose()}finally{await t.dispose()}})())}function d(e,t){l(e,t.pi)}exports.default=u,exports.userFeedbackExtension=u,exports.installUserFeedbackRuntime=l;
2
+ //# sourceMappingURL=extension.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.cjs","names":["QuestionnaireCoordinator","DOOM_MINOR_MODE_CATALOG_SERVICE","DOOM_NARRATION_SERVICE","createVoiceQuestionHandoff","requireMinorModeCatalog","requireDoomNarrationService","connectDoomCordisHost"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport { DOOM_MINOR_MODE_CATALOG_SERVICE, requireMinorModeCatalog } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { DOOM_NARRATION_SERVICE, requireDoomNarrationService } from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { QuestionnaireCoordinator, type QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { createVoiceQuestionHandoff, type VoiceQuestionHandoff } from '../doom/voiceQuestionHandoff.js';\nimport { registerAskUserQuestionTool } from './askUserQuestionAdapter.js';\nimport { registerAskUserQuestionReconciler } from './reconcileAdapter.js';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-user-feedback';\nconst SESSION_START_EVENT = 'session_start';\n\nfunction cancelledResult(): QuestionnaireResult {\n return { answers: [], cancelled: true };\n}\n\n/** Install one user-feedback runtime into its host-owned Cordis plugin fiber. */\nexport function installUserFeedbackRuntime(cordis: Context, pi: ExtensionAPI): void {\n let active = true;\n let sessionGeneration = 0;\n let sessionId: string | undefined;\n let sessionReady = false;\n let coordinator = new QuestionnaireCoordinator();\n let voiceHandoff: VoiceQuestionHandoff | undefined;\n let lifecycleQueue: Promise<void> = Promise.resolve();\n const pendingOperations = new Set<Promise<unknown>>();\n\n const trackOperation = <T>(operation: Promise<T>): Promise<T> => {\n pendingOperations.add(operation);\n void operation.then(\n () => pendingOperations.delete(operation),\n () => pendingOperations.delete(operation),\n );\n return operation;\n };\n const ownsGeneration = (generation: number, expectedSessionId: string | undefined): boolean =>\n active && generation === sessionGeneration && expectedSessionId === sessionId;\n const isContextActive = (context: ExtensionContext, signal?: AbortSignal): boolean =>\n active && sessionReady && signal?.aborted !== true && context.sessionManager.getSessionId() === sessionId;\n\n const shutdownRuntime = async (): Promise<void> => {\n if (!active) return;\n const ownedCoordinator = coordinator;\n ownedCoordinator.shutdown();\n active = false;\n sessionGeneration += 1;\n sessionReady = false;\n sessionId = undefined;\n await Promise.allSettled(pendingOperations);\n await ownedCoordinator.waitForIdle();\n };\n cordis.effect(() => shutdownRuntime, `${PACKAGE_SOURCE}/runtime`);\n\n cordis.inject([DOOM_MINOR_MODE_CATALOG_SERVICE, DOOM_NARRATION_SERVICE], (serviceContext) => {\n const handoff = createVoiceQuestionHandoff(\n requireMinorModeCatalog(serviceContext),\n requireDoomNarrationService(serviceContext),\n );\n voiceHandoff = handoff;\n return () => {\n if (voiceHandoff === handoff) voiceHandoff = undefined;\n };\n });\n\n registerAskUserQuestionTool(pi, cordis, {\n enqueue: (runner: QuestionnaireRunner, signal?: AbortSignal) => {\n const ownedCoordinator = coordinator;\n const generation = sessionGeneration;\n const expectedSessionId = sessionId;\n if (!sessionReady || !ownsGeneration(generation, expectedSessionId) || signal?.aborted) {\n return Promise.resolve(cancelledResult());\n }\n return ownedCoordinator.enqueue(async (context) => {\n if (!sessionReady || !ownsGeneration(generation, expectedSessionId) || context.signal.aborted) {\n return cancelledResult();\n }\n const result = await runner({\n ...context,\n reportProgress: (progress) => {\n if (ownsGeneration(generation, expectedSessionId) && !context.signal.aborted) {\n context.reportProgress(progress);\n }\n },\n });\n return sessionReady && ownsGeneration(generation, expectedSessionId) && !context.signal.aborted\n ? result\n : cancelledResult();\n }, signal);\n },\n isActive: isContextActive,\n runTui: async (context, params, collapseKey, signal, reportProgress) => {\n if (!isContextActive(context, signal)) return undefined;\n const { runTuiQuestionnaire } = await import('../../tui/runQuestionnaire.js');\n if (!isContextActive(context, signal)) return undefined;\n const result = await runTuiQuestionnaire(context, params, collapseKey, signal, reportProgress);\n return isContextActive(context, signal) ? result : undefined;\n },\n tryVoice: (params) => (active && sessionReady ? voiceHandoff?.handoff(params) : undefined),\n });\n registerAskUserQuestionReconciler(pi, isContextActive);\n\n pi.on(SESSION_START_EVENT, (_event, context) => {\n if (!active) return;\n const previousCoordinator = coordinator;\n previousCoordinator.shutdown();\n const activeSessionId = context.sessionManager.getSessionId();\n const generation = ++sessionGeneration;\n const nextCoordinator = new QuestionnaireCoordinator();\n coordinator = nextCoordinator;\n sessionId = activeSessionId;\n sessionReady = false;\n\n const initializeSession = async (): Promise<void> => {\n await previousCoordinator.waitForIdle();\n if (!ownsGeneration(generation, activeSessionId)) return;\n sessionReady = true;\n };\n\n const operation = lifecycleQueue.then(initializeSession, initializeSession);\n lifecycleQueue = operation.catch(() => undefined);\n return trackOperation(operation);\n });\n}\n\n/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */\nexport async function userFeedbackExtension(pi: ExtensionAPI): Promise<void> {\n const connection = await connectDoomCordisHost(pi, PACKAGE_SOURCE);\n const fiber = connection.root.plugin(userFeedbackPlugin, { pi });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n\ninterface UserFeedbackPluginConfig {\n readonly pi: ExtensionAPI;\n}\n\nfunction userFeedbackPlugin(cordis: Context, config: UserFeedbackPluginConfig): void {\n installUserFeedbackRuntime(cordis, config.pi);\n}\n\nexport default userFeedbackExtension;\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import "@deepseek-ai/cordis";
2
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ //#region src/adapters/pi/extension.d.ts
4
+ /** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */
5
+ declare function userFeedbackExtension(pi: ExtensionAPI): Promise<void>;
6
+ //#endregion
7
+ export { userFeedbackExtension as default, userFeedbackExtension };
8
+ //# sourceMappingURL=extension.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;iBA+HsB,sBAAsB,IAAI,eAAe"}
@@ -0,0 +1,8 @@
1
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import "@deepseek-ai/cordis";
3
+ //#region src/adapters/pi/extension.d.ts
4
+ /** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */
5
+ declare function userFeedbackExtension(pi: ExtensionAPI): Promise<void>;
6
+ //#endregion
7
+ export { userFeedbackExtension as default, userFeedbackExtension };
8
+ //# sourceMappingURL=extension.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;iBA+HsB,sBAAsB,IAAI,eAAe"}
@@ -0,0 +1,2 @@
1
+ import{QuestionnaireCoordinator as e}from"../../services/questionnaireCoordinator.mjs";import{createVoiceQuestionHandoff as t}from"../doom/voiceQuestionHandoff.mjs";import{registerAskUserQuestionTool as n}from"./askUserQuestionAdapter.mjs";import{registerAskUserQuestionReconciler as r}from"./reconcileAdapter.mjs";import{connectDoomCordisHost as i}from"@agimon-ai/doompi-extension-contracts/cordis-host";import{DOOM_MINOR_MODE_CATALOG_SERVICE as a,requireMinorModeCatalog as o}from"@agimon-ai/doompi-extension-contracts/mode";import{DOOM_NARRATION_SERVICE as s,requireDoomNarrationService as c}from"@agimon-ai/doompi-extension-contracts/narration";const l=`@agimon-ai/doompi-user-feedback`;function u(){return{answers:[],cancelled:!0}}function d(i,d){let f=!0,p=0,m,h=!1,g=new e,_,v=Promise.resolve(),y=new Set,b=e=>(y.add(e),e.then(()=>y.delete(e),()=>y.delete(e)),e),x=(e,t)=>f&&e===p&&t===m,S=(e,t)=>f&&h&&t?.aborted!==!0&&e.sessionManager.getSessionId()===m,C=async()=>{if(!f)return;let e=g;e.shutdown(),f=!1,p+=1,h=!1,m=void 0,await Promise.allSettled(y),await e.waitForIdle()};i.effect(()=>C,`${l}/runtime`),i.inject([a,s],e=>{let n=t(o(e),c(e));return _=n,()=>{_===n&&(_=void 0)}}),n(d,i,{enqueue:(e,t)=>{let n=g,r=p,i=m;return!h||!x(r,i)||t?.aborted?Promise.resolve(u()):n.enqueue(async t=>{if(!h||!x(r,i)||t.signal.aborted)return u();let n=await e({...t,reportProgress:e=>{x(r,i)&&!t.signal.aborted&&t.reportProgress(e)}});return h&&x(r,i)&&!t.signal.aborted?n:u()},t)},isActive:S,runTui:async(e,t,n,r,i)=>{if(!S(e,r))return;let{runTuiQuestionnaire:a}=await import(`../../tui/runQuestionnaire.mjs`);if(!S(e,r))return;let o=await a(e,t,n,r,i);return S(e,r)?o:void 0},tryVoice:e=>f&&h?_?.handoff(e):void 0}),r(d,S),d.on(`session_start`,(t,n)=>{if(!f)return;let r=g;r.shutdown();let i=n.sessionManager.getSessionId(),a=++p;g=new e,m=i,h=!1;let o=async()=>{await r.waitForIdle(),x(a,i)&&(h=!0)},s=v.then(o,o);return v=s.catch(()=>void 0),b(s)})}async function f(e){let t=await i(e,l),n=t.root.plugin(p,{pi:e});try{await n}catch(e){try{await n.dispose()}finally{await t.dispose()}throw e}let r;e.on(`session_shutdown`,()=>r??=(async()=>{try{await n.dispose()}finally{await t.dispose()}})())}function p(e,t){d(e,t.pi)}export{f as default,f as userFeedbackExtension,d as installUserFeedbackRuntime};
2
+ //# sourceMappingURL=extension.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport { DOOM_MINOR_MODE_CATALOG_SERVICE, requireMinorModeCatalog } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { DOOM_NARRATION_SERVICE, requireDoomNarrationService } from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { QuestionnaireCoordinator, type QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { createVoiceQuestionHandoff, type VoiceQuestionHandoff } from '../doom/voiceQuestionHandoff.js';\nimport { registerAskUserQuestionTool } from './askUserQuestionAdapter.js';\nimport { registerAskUserQuestionReconciler } from './reconcileAdapter.js';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-user-feedback';\nconst SESSION_START_EVENT = 'session_start';\n\nfunction cancelledResult(): QuestionnaireResult {\n return { answers: [], cancelled: true };\n}\n\n/** Install one user-feedback runtime into its host-owned Cordis plugin fiber. */\nexport function installUserFeedbackRuntime(cordis: Context, pi: ExtensionAPI): void {\n let active = true;\n let sessionGeneration = 0;\n let sessionId: string | undefined;\n let sessionReady = false;\n let coordinator = new QuestionnaireCoordinator();\n let voiceHandoff: VoiceQuestionHandoff | undefined;\n let lifecycleQueue: Promise<void> = Promise.resolve();\n const pendingOperations = new Set<Promise<unknown>>();\n\n const trackOperation = <T>(operation: Promise<T>): Promise<T> => {\n pendingOperations.add(operation);\n void operation.then(\n () => pendingOperations.delete(operation),\n () => pendingOperations.delete(operation),\n );\n return operation;\n };\n const ownsGeneration = (generation: number, expectedSessionId: string | undefined): boolean =>\n active && generation === sessionGeneration && expectedSessionId === sessionId;\n const isContextActive = (context: ExtensionContext, signal?: AbortSignal): boolean =>\n active && sessionReady && signal?.aborted !== true && context.sessionManager.getSessionId() === sessionId;\n\n const shutdownRuntime = async (): Promise<void> => {\n if (!active) return;\n const ownedCoordinator = coordinator;\n ownedCoordinator.shutdown();\n active = false;\n sessionGeneration += 1;\n sessionReady = false;\n sessionId = undefined;\n await Promise.allSettled(pendingOperations);\n await ownedCoordinator.waitForIdle();\n };\n cordis.effect(() => shutdownRuntime, `${PACKAGE_SOURCE}/runtime`);\n\n cordis.inject([DOOM_MINOR_MODE_CATALOG_SERVICE, DOOM_NARRATION_SERVICE], (serviceContext) => {\n const handoff = createVoiceQuestionHandoff(\n requireMinorModeCatalog(serviceContext),\n requireDoomNarrationService(serviceContext),\n );\n voiceHandoff = handoff;\n return () => {\n if (voiceHandoff === handoff) voiceHandoff = undefined;\n };\n });\n\n registerAskUserQuestionTool(pi, cordis, {\n enqueue: (runner: QuestionnaireRunner, signal?: AbortSignal) => {\n const ownedCoordinator = coordinator;\n const generation = sessionGeneration;\n const expectedSessionId = sessionId;\n if (!sessionReady || !ownsGeneration(generation, expectedSessionId) || signal?.aborted) {\n return Promise.resolve(cancelledResult());\n }\n return ownedCoordinator.enqueue(async (context) => {\n if (!sessionReady || !ownsGeneration(generation, expectedSessionId) || context.signal.aborted) {\n return cancelledResult();\n }\n const result = await runner({\n ...context,\n reportProgress: (progress) => {\n if (ownsGeneration(generation, expectedSessionId) && !context.signal.aborted) {\n context.reportProgress(progress);\n }\n },\n });\n return sessionReady && ownsGeneration(generation, expectedSessionId) && !context.signal.aborted\n ? result\n : cancelledResult();\n }, signal);\n },\n isActive: isContextActive,\n runTui: async (context, params, collapseKey, signal, reportProgress) => {\n if (!isContextActive(context, signal)) return undefined;\n const { runTuiQuestionnaire } = await import('../../tui/runQuestionnaire.js');\n if (!isContextActive(context, signal)) return undefined;\n const result = await runTuiQuestionnaire(context, params, collapseKey, signal, reportProgress);\n return isContextActive(context, signal) ? result : undefined;\n },\n tryVoice: (params) => (active && sessionReady ? voiceHandoff?.handoff(params) : undefined),\n });\n registerAskUserQuestionReconciler(pi, isContextActive);\n\n pi.on(SESSION_START_EVENT, (_event, context) => {\n if (!active) return;\n const previousCoordinator = coordinator;\n previousCoordinator.shutdown();\n const activeSessionId = context.sessionManager.getSessionId();\n const generation = ++sessionGeneration;\n const nextCoordinator = new QuestionnaireCoordinator();\n coordinator = nextCoordinator;\n sessionId = activeSessionId;\n sessionReady = false;\n\n const initializeSession = async (): Promise<void> => {\n await previousCoordinator.waitForIdle();\n if (!ownsGeneration(generation, activeSessionId)) return;\n sessionReady = true;\n };\n\n const operation = lifecycleQueue.then(initializeSession, initializeSession);\n lifecycleQueue = operation.catch(() => undefined);\n return trackOperation(operation);\n });\n}\n\n/** The package's sole Pi factory; Pi reloads it and Cordis owns all package resources. */\nexport async function userFeedbackExtension(pi: ExtensionAPI): Promise<void> {\n const connection = await connectDoomCordisHost(pi, PACKAGE_SOURCE);\n const fiber = connection.root.plugin(userFeedbackPlugin, { pi });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n\ninterface UserFeedbackPluginConfig {\n readonly pi: ExtensionAPI;\n}\n\nfunction userFeedbackPlugin(cordis: Context, config: UserFeedbackPluginConfig): void {\n installUserFeedbackRuntime(cordis, config.pi);\n}\n\nexport default userFeedbackExtension;\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./askUserQuestionAdapter.cjs");function t(t,n){let r=t.getActiveTools(),i=r.includes(e.ASK_USER_QUESTION_TOOL_NAME);if(!n.hasUI&&i){t.setActiveTools(r.filter(t=>t!==e.ASK_USER_QUESTION_TOOL_NAME));return}n.hasUI&&!i&&t.setActiveTools([...r,e.ASK_USER_QUESTION_TOOL_NAME])}function n(e){e.on(`before_agent_start`,(n,r)=>{t(e,r)})}exports.reconcileAskUserQuestionTool=t,exports.registerAskUserQuestionReconciler=n;
1
+ const e=require("./askUserQuestionAdapter.cjs");function t(t,n){let r=t.getActiveTools(),i=r.includes(e.ASK_USER_QUESTION_TOOL_NAME);if(!n.hasUI&&i){t.setActiveTools(r.filter(t=>t!==e.ASK_USER_QUESTION_TOOL_NAME));return}n.hasUI&&!i&&t.setActiveTools([...r,e.ASK_USER_QUESTION_TOOL_NAME])}function n(e,n=()=>!0){e.on(`before_agent_start`,(r,i)=>{n(i)&&t(e,i)})}exports.reconcileAskUserQuestionTool=t,exports.registerAskUserQuestionReconciler=n;
2
2
  //# sourceMappingURL=reconcileAdapter.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcileAdapter.cjs","names":["ASK_USER_QUESTION_TOOL_NAME"],"sources":["../../../src/adapters/pi/reconcileAdapter.ts"],"sourcesContent":["import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { ASK_USER_QUESTION_TOOL_NAME } from './askUserQuestionAdapter.js';\n\nexport function reconcileAskUserQuestionTool(pi: ExtensionAPI, context: Pick<ExtensionContext, 'hasUI'>): void {\n const activeTools = pi.getActiveTools();\n const active = activeTools.includes(ASK_USER_QUESTION_TOOL_NAME);\n if (!context.hasUI && active) {\n pi.setActiveTools(activeTools.filter((name) => name !== ASK_USER_QUESTION_TOOL_NAME));\n return;\n }\n if (context.hasUI && !active) {\n pi.setActiveTools([...activeTools, ASK_USER_QUESTION_TOOL_NAME]);\n }\n}\n\nexport function registerAskUserQuestionReconciler(pi: ExtensionAPI): void {\n pi.on('before_agent_start', (_event, context) => {\n reconcileAskUserQuestionTool(pi, context);\n });\n}\n"],"mappings":"gDAGA,SAAgB,EAA6B,EAAkB,EAAgD,CAC7G,IAAM,EAAc,EAAG,eAAe,EAChC,EAAS,EAAY,SAASA,EAAAA,2BAA2B,EAC/D,GAAI,CAAC,EAAQ,OAAS,EAAQ,CAC5B,EAAG,eAAe,EAAY,OAAQ,GAAS,IAASA,EAAAA,2BAA2B,CAAC,EACpF,MACF,CACI,EAAQ,OAAS,CAAC,GACpB,EAAG,eAAe,CAAC,GAAG,EAAaA,EAAAA,2BAA2B,CAAC,CAEnE,CAEA,SAAgB,EAAkC,EAAwB,CACxE,EAAG,GAAG,sBAAuB,EAAQ,IAAY,CAC/C,EAA6B,EAAI,CAAO,CAC1C,CAAC,CACH"}
1
+ {"version":3,"file":"reconcileAdapter.cjs","names":["ASK_USER_QUESTION_TOOL_NAME"],"sources":["../../../src/adapters/pi/reconcileAdapter.ts"],"sourcesContent":["import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { ASK_USER_QUESTION_TOOL_NAME } from './askUserQuestionAdapter.js';\n\nexport function reconcileAskUserQuestionTool(pi: ExtensionAPI, context: Pick<ExtensionContext, 'hasUI'>): void {\n const activeTools = pi.getActiveTools();\n const active = activeTools.includes(ASK_USER_QUESTION_TOOL_NAME);\n if (!context.hasUI && active) {\n pi.setActiveTools(activeTools.filter((name) => name !== ASK_USER_QUESTION_TOOL_NAME));\n return;\n }\n if (context.hasUI && !active) {\n pi.setActiveTools([...activeTools, ASK_USER_QUESTION_TOOL_NAME]);\n }\n}\n\nexport function registerAskUserQuestionReconciler(\n pi: ExtensionAPI,\n isActive: (context: ExtensionContext) => boolean = () => true,\n): void {\n pi.on('before_agent_start', (_event, context) => {\n if (isActive(context)) reconcileAskUserQuestionTool(pi, context);\n });\n}\n"],"mappings":"gDAGA,SAAgB,EAA6B,EAAkB,EAAgD,CAC7G,IAAM,EAAc,EAAG,eAAe,EAChC,EAAS,EAAY,SAASA,EAAAA,2BAA2B,EAC/D,GAAI,CAAC,EAAQ,OAAS,EAAQ,CAC5B,EAAG,eAAe,EAAY,OAAQ,GAAS,IAASA,EAAAA,2BAA2B,CAAC,EACpF,MACF,CACI,EAAQ,OAAS,CAAC,GACpB,EAAG,eAAe,CAAC,GAAG,EAAaA,EAAAA,2BAA2B,CAAC,CAEnE,CAEA,SAAgB,EACd,EACA,MAAyD,GACnD,CACN,EAAG,GAAG,sBAAuB,EAAQ,IAAY,CAC3C,EAAS,CAAO,GAAG,EAA6B,EAAI,CAAO,CACjE,CAAC,CACH"}
@@ -1,2 +1,2 @@
1
- import{ASK_USER_QUESTION_TOOL_NAME as e}from"./askUserQuestionAdapter.mjs";function t(t,n){let r=t.getActiveTools(),i=r.includes(e);if(!n.hasUI&&i){t.setActiveTools(r.filter(t=>t!==e));return}n.hasUI&&!i&&t.setActiveTools([...r,e])}function n(e){e.on(`before_agent_start`,(n,r)=>{t(e,r)})}export{t as reconcileAskUserQuestionTool,n as registerAskUserQuestionReconciler};
1
+ import{ASK_USER_QUESTION_TOOL_NAME as e}from"./askUserQuestionAdapter.mjs";function t(t,n){let r=t.getActiveTools(),i=r.includes(e);if(!n.hasUI&&i){t.setActiveTools(r.filter(t=>t!==e));return}n.hasUI&&!i&&t.setActiveTools([...r,e])}function n(e,n=()=>!0){e.on(`before_agent_start`,(r,i)=>{n(i)&&t(e,i)})}export{t as reconcileAskUserQuestionTool,n as registerAskUserQuestionReconciler};
2
2
  //# sourceMappingURL=reconcileAdapter.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcileAdapter.mjs","names":[],"sources":["../../../src/adapters/pi/reconcileAdapter.ts"],"sourcesContent":["import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { ASK_USER_QUESTION_TOOL_NAME } from './askUserQuestionAdapter.js';\n\nexport function reconcileAskUserQuestionTool(pi: ExtensionAPI, context: Pick<ExtensionContext, 'hasUI'>): void {\n const activeTools = pi.getActiveTools();\n const active = activeTools.includes(ASK_USER_QUESTION_TOOL_NAME);\n if (!context.hasUI && active) {\n pi.setActiveTools(activeTools.filter((name) => name !== ASK_USER_QUESTION_TOOL_NAME));\n return;\n }\n if (context.hasUI && !active) {\n pi.setActiveTools([...activeTools, ASK_USER_QUESTION_TOOL_NAME]);\n }\n}\n\nexport function registerAskUserQuestionReconciler(pi: ExtensionAPI): void {\n pi.on('before_agent_start', (_event, context) => {\n reconcileAskUserQuestionTool(pi, context);\n });\n}\n"],"mappings":"2EAGA,SAAgB,EAA6B,EAAkB,EAAgD,CAC7G,IAAM,EAAc,EAAG,eAAe,EAChC,EAAS,EAAY,SAAS,CAA2B,EAC/D,GAAI,CAAC,EAAQ,OAAS,EAAQ,CAC5B,EAAG,eAAe,EAAY,OAAQ,GAAS,IAAS,CAA2B,CAAC,EACpF,MACF,CACI,EAAQ,OAAS,CAAC,GACpB,EAAG,eAAe,CAAC,GAAG,EAAa,CAA2B,CAAC,CAEnE,CAEA,SAAgB,EAAkC,EAAwB,CACxE,EAAG,GAAG,sBAAuB,EAAQ,IAAY,CAC/C,EAA6B,EAAI,CAAO,CAC1C,CAAC,CACH"}
1
+ {"version":3,"file":"reconcileAdapter.mjs","names":[],"sources":["../../../src/adapters/pi/reconcileAdapter.ts"],"sourcesContent":["import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { ASK_USER_QUESTION_TOOL_NAME } from './askUserQuestionAdapter.js';\n\nexport function reconcileAskUserQuestionTool(pi: ExtensionAPI, context: Pick<ExtensionContext, 'hasUI'>): void {\n const activeTools = pi.getActiveTools();\n const active = activeTools.includes(ASK_USER_QUESTION_TOOL_NAME);\n if (!context.hasUI && active) {\n pi.setActiveTools(activeTools.filter((name) => name !== ASK_USER_QUESTION_TOOL_NAME));\n return;\n }\n if (context.hasUI && !active) {\n pi.setActiveTools([...activeTools, ASK_USER_QUESTION_TOOL_NAME]);\n }\n}\n\nexport function registerAskUserQuestionReconciler(\n pi: ExtensionAPI,\n isActive: (context: ExtensionContext) => boolean = () => true,\n): void {\n pi.on('before_agent_start', (_event, context) => {\n if (isActive(context)) reconcileAskUserQuestionTool(pi, context);\n });\n}\n"],"mappings":"2EAGA,SAAgB,EAA6B,EAAkB,EAAgD,CAC7G,IAAM,EAAc,EAAG,eAAe,EAChC,EAAS,EAAY,SAAS,CAA2B,EAC/D,GAAI,CAAC,EAAQ,OAAS,EAAQ,CAC5B,EAAG,eAAe,EAAY,OAAQ,GAAS,IAAS,CAA2B,CAAC,EACpF,MACF,CACI,EAAQ,OAAS,CAAC,GACpB,EAAG,eAAe,CAAC,GAAG,EAAa,CAA2B,CAAC,CAEnE,CAEA,SAAgB,EACd,EACA,MAAyD,GACnD,CACN,EAAG,GAAG,sBAAuB,EAAQ,IAAY,CAC3C,EAAS,CAAO,GAAG,EAA6B,EAAI,CAAO,CACjE,CAAC,CACH"}
@@ -1,2 +1 @@
1
- Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("../adapters/doom/voiceQuestionHandoff.cjs"),t=require("../adapters/pi/askUserQuestionAdapter.cjs"),n=require("../adapters/pi/reconcileAdapter.cjs"),r=require("../services/questionnaireCoordinator.cjs"),i=require("../services/voiceQuestionNarration.cjs");let a=require("@agimon-ai/doompi-extension-contracts/mode"),o=require("@agimon-ai/doompi-extension-contracts/narration"),s=require("@agimon-ai/doompi-extension-contracts/protocol");const c=`@agimon-ai/doompi-user-feedback`,l=Symbol.for(`${c}/installed-hosts`);function u(e){let t=globalThis,n=t[l]??new WeakSet;return t[l]=n,!n.has(e)&&(n.add(e),!0)}function d(l){if(!u(l))return;let d=new r.QuestionnaireCoordinator,f,p,m,h=()=>{p?.(),p=void 0,f?.reset(),f=void 0,m=void 0};t.registerAskUserQuestionTool(l,{enqueue:(e,t)=>d.enqueue(e,t),runTui:async(...e)=>{let{runTuiQuestionnaire:t}=await Promise.resolve().then(()=>require("../tui/runQuestionnaire.cjs"));return t(...e)},tryVoice:e=>m?.handoff(e)}),n.registerAskUserQuestionReconciler(l),l.on(`session_start`,(t,n)=>{d.shutdown(),d=new r.QuestionnaireCoordinator,h();let u=n.sessionManager.getSessionId(),g=new a.DoomModeStatusRegistry,_=(0,s.createProtocolRuntime)({bus:l.events,source:c,sessionId:u});f=g,p=g.bind(_),m=new e.VoiceQuestionHandoff(g,(0,o.createNarrationRequester)({bus:l.events,source:c,sessionId:u}),i.buildVoiceQuestionNarration)}),l.on(`session_shutdown`,()=>{d.shutdown(),h()})}exports.default=d,exports.registerUserFeedbackExtension=d;
2
- //# sourceMappingURL=pi.cjs.map
1
+ const e=require("../adapters/pi/extension.cjs");module.exports=e.default;
@@ -1,6 +1,2 @@
1
- import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- //#region src/extensions/pi.d.ts
3
- declare function registerUserFeedbackExtension(pi: ExtensionAPI): void;
4
- //#endregion
5
- export { registerUserFeedbackExtension as default, registerUserFeedbackExtension };
6
- //# sourceMappingURL=pi.d.cts.map
1
+ import userFeedbackExtension from "../adapters/pi/extension.cjs";
2
+ export = userFeedbackExtension;
@@ -1,6 +1,2 @@
1
- import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- //#region src/extensions/pi.d.ts
3
- declare function registerUserFeedbackExtension(pi: ExtensionAPI): void;
4
- //#endregion
5
- export { registerUserFeedbackExtension as default, registerUserFeedbackExtension };
6
- //# sourceMappingURL=pi.d.mts.map
1
+ import userFeedbackExtension from "../adapters/pi/extension.mjs";
2
+ export { userFeedbackExtension as default };
@@ -1,2 +1 @@
1
- import{VoiceQuestionHandoff as e}from"../adapters/doom/voiceQuestionHandoff.mjs";import{registerAskUserQuestionTool as t}from"../adapters/pi/askUserQuestionAdapter.mjs";import{registerAskUserQuestionReconciler as n}from"../adapters/pi/reconcileAdapter.mjs";import{QuestionnaireCoordinator as r}from"../services/questionnaireCoordinator.mjs";import{buildVoiceQuestionNarration as i}from"../services/voiceQuestionNarration.mjs";import{DoomModeStatusRegistry as a}from"@agimon-ai/doompi-extension-contracts/mode";import{createNarrationRequester as o}from"@agimon-ai/doompi-extension-contracts/narration";import{createProtocolRuntime as s}from"@agimon-ai/doompi-extension-contracts/protocol";const c=`@agimon-ai/doompi-user-feedback`,l=Symbol.for(`${c}/installed-hosts`);function u(e){let t=globalThis,n=t[l]??new WeakSet;return t[l]=n,!n.has(e)&&(n.add(e),!0)}function d(l){if(!u(l))return;let d=new r,f,p,m,h=()=>{p?.(),p=void 0,f?.reset(),f=void 0,m=void 0};t(l,{enqueue:(e,t)=>d.enqueue(e,t),runTui:async(...e)=>{let{runTuiQuestionnaire:t}=await import(`../tui/runQuestionnaire.mjs`);return t(...e)},tryVoice:e=>m?.handoff(e)}),n(l),l.on(`session_start`,(t,n)=>{d.shutdown(),d=new r,h();let u=n.sessionManager.getSessionId(),g=new a,_=s({bus:l.events,source:c,sessionId:u});f=g,p=g.bind(_),m=new e(g,o({bus:l.events,source:c,sessionId:u}),i)}),l.on(`session_shutdown`,()=>{d.shutdown(),h()})}export{d as default,d as registerUserFeedbackExtension};
2
- //# sourceMappingURL=pi.mjs.map
1
+ import e from"../adapters/pi/extension.mjs";export{e as default};
package/dist/index.cjs CHANGED
@@ -1 +0,0 @@
1
- Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./extensions/pi.cjs");require("./events.cjs");let t=require("@agimon-ai/doompi-extension-contracts");Object.defineProperty(exports,"ASK_USER_BLOCKED_EVENT",{enumerable:!0,get:function(){return t.ASK_USER_BLOCKED_EVENT}}),Object.defineProperty(exports,"ASK_USER_PROMPT_EVENT",{enumerable:!0,get:function(){return t.ASK_USER_PROMPT_EVENT}}),exports.default=e.default,exports.registerUserFeedbackExtension=e.default;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,3 @@
1
- import { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, AskUserBlockedEventPayload, AskUserPromptEventPayload, AskUserPromptOption, AskUserPromptQuestion } from "./schemas/events.cjs";
2
- import "./events.cjs";
3
- import registerUserFeedbackExtension from "./extensions/pi.cjs";
4
- export { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, type AskUserBlockedEventPayload, type AskUserPromptEventPayload, type AskUserPromptOption, type AskUserPromptQuestion, registerUserFeedbackExtension as default, registerUserFeedbackExtension };
1
+ import { QuestionParams } from "./schemas/questionnaire.cjs";
2
+ import { QuestionnaireResult, ToolTextResult } from "./types/questionnaire.cjs";
3
+ export type { QuestionParams, QuestionnaireResult, ToolTextResult };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- import { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, AskUserBlockedEventPayload, AskUserPromptEventPayload, AskUserPromptOption, AskUserPromptQuestion } from "./schemas/events.mjs";
2
- import "./events.mjs";
3
- import registerUserFeedbackExtension from "./extensions/pi.mjs";
4
- export { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, type AskUserBlockedEventPayload, type AskUserPromptEventPayload, type AskUserPromptOption, type AskUserPromptQuestion, registerUserFeedbackExtension as default, registerUserFeedbackExtension };
1
+ import { QuestionParams } from "./schemas/questionnaire.mjs";
2
+ import { QuestionnaireResult, ToolTextResult } from "./types/questionnaire.mjs";
3
+ export type { QuestionParams, QuestionnaireResult, ToolTextResult };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import e from"./extensions/pi.mjs";import{ASK_USER_BLOCKED_EVENT as t,ASK_USER_PROMPT_EVENT as n}from"./schemas/events.mjs";import"./events.mjs";export{t as ASK_USER_BLOCKED_EVENT,n as ASK_USER_PROMPT_EVENT,e as default,e as registerUserFeedbackExtension};
1
+ export{};
@@ -0,0 +1,18 @@
1
+ import { Static, Type } from "typebox";
2
+ //#region src/schemas/questionnaire.d.ts
3
+ declare const QuestionParamsSchema: Type.TObject<{
4
+ questions: Type.TArray<Type.TObject<{
5
+ question: Type.TString;
6
+ header: Type.TString;
7
+ options: Type.TArray<Type.TObject<{
8
+ label: Type.TString;
9
+ description: Type.TString;
10
+ preview: Type.TOptional<Type.TString>;
11
+ }>>;
12
+ multiSelect: Type.TOptional<Type.TBoolean>;
13
+ }>>;
14
+ }>;
15
+ type QuestionParams = Static<typeof QuestionParamsSchema>;
16
+ //#endregion
17
+ export { QuestionParams, QuestionParamsSchema };
18
+ //# sourceMappingURL=questionnaire.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questionnaire.d.cts","names":[],"sources":["../../src/schemas/questionnaire.ts"],"mappings":";;cA6Ca,sBAAoB,KAAA;;;;;;;;;;;;KAarB,iBAAiB,cAAc"}
@@ -0,0 +1,18 @@
1
+ import { Static, Type } from "typebox";
2
+ //#region src/schemas/questionnaire.d.ts
3
+ declare const QuestionParamsSchema: Type.TObject<{
4
+ questions: Type.TArray<Type.TObject<{
5
+ question: Type.TString;
6
+ header: Type.TString;
7
+ options: Type.TArray<Type.TObject<{
8
+ label: Type.TString;
9
+ description: Type.TString;
10
+ preview: Type.TOptional<Type.TString>;
11
+ }>>;
12
+ multiSelect: Type.TOptional<Type.TBoolean>;
13
+ }>>;
14
+ }>;
15
+ type QuestionParams = Static<typeof QuestionParamsSchema>;
16
+ //#endregion
17
+ export { QuestionParams, QuestionParamsSchema };
18
+ //# sourceMappingURL=questionnaire.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questionnaire.d.mts","names":[],"sources":["../../src/schemas/questionnaire.ts"],"mappings":";;cA6Ca,sBAAoB,KAAA;;;;;;;;;;;;KAarB,iBAAiB,cAAc"}
@@ -1,2 +1,2 @@
1
- function e(e){return{answers:e.latestAnswers.map(e=>({...e})),cancelled:!0}}var t=class{pending=[];active;nextRequestId=1;accepting=!0;enqueue(e,t){return!this.accepting||t?.aborted?Promise.resolve({answers:[],cancelled:!0}):new Promise((n,r)=>{let i={id:this.nextRequestId,runner:e,controller:new AbortController,...t?{externalSignal:t}:{},latestAnswers:[],state:`queued`,resolve:n,reject:r};this.nextRequestId+=1;let a=()=>this.cancel(i);t?.addEventListener(`abort`,a,{once:!0}),i.removeAbortListener=()=>t?.removeEventListener(`abort`,a),this.pending.push(i),this.startNext()})}shutdown(){if(!(!this.accepting&&!this.active&&this.pending.length===0))for(this.accepting=!1,this.active&&this.cancel(this.active);this.pending.length>0;){let e=this.pending[0];if(!e)break;this.cancel(e)}}startNext(){if(!this.accepting||this.active)return;let e=this.pending.shift();e&&(e.state=`active`,this.active=e,e.runner({signal:e.controller.signal,reportProgress:t=>{this.active?.id===e.id&&e.state===`active`&&(e.latestAnswers=t.answers.map(e=>({...e})))}}).then(t=>this.complete(e,t),t=>this.fail(e,t)))}complete(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.resolve(t),this.startNext())}fail(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.reject(t),this.startNext())}cancel(t){if(t.state===`settled`)return;if(t.state===`queued`){let n=this.pending.indexOf(t);this.pending.splice(n,1),t.state=`settled`,t.removeAbortListener?.(),t.controller.abort(),t.resolve(e(t));return}let n=e(t);this.releaseActive(t),t.controller.abort(),t.resolve(n),this.startNext()}releaseActive(e){e.state=`settled`,e.removeAbortListener?.(),this.active=void 0}};exports.QuestionnaireCoordinator=t;
1
+ function e(e){return{answers:e.latestAnswers.map(e=>({...e})),cancelled:!0}}var t=class{pending=[];running=new Set;active;nextRequestId=1;accepting=!0;enqueue(e,t){return!this.accepting||t?.aborted?Promise.resolve({answers:[],cancelled:!0}):new Promise((n,r)=>{let i={id:this.nextRequestId,runner:e,controller:new AbortController,...t?{externalSignal:t}:{},latestAnswers:[],state:`queued`,resolve:n,reject:r};this.nextRequestId+=1;let a=()=>this.cancel(i);t?.addEventListener(`abort`,a,{once:!0}),i.removeAbortListener=()=>t?.removeEventListener(`abort`,a),this.pending.push(i),this.startNext()})}shutdown(){if(!(!this.accepting&&!this.active&&this.pending.length===0))for(this.accepting=!1,this.active&&this.cancel(this.active);this.pending.length>0;){let e=this.pending[0];if(!e)break;this.cancel(e)}}async waitForIdle(){for(;this.running.size>0;)await Promise.allSettled(this.running)}startNext(){if(!this.accepting||this.active)return;let e=this.pending.shift();if(!e)return;e.state=`active`,this.active=e;let t=t=>{this.active?.id===e.id&&e.state===`active`&&(e.latestAnswers=t.answers.map(e=>({...e})))},n;try{n=e.runner({signal:e.controller.signal,reportProgress:t})}catch(e){n=Promise.reject(e)}this.running.add(n),n.then(t=>this.complete(e,t),t=>this.fail(e,t)).finally(()=>this.running.delete(n))}complete(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.resolve(t),this.startNext())}fail(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.reject(t),this.startNext())}cancel(t){if(t.state===`settled`)return;if(t.state===`queued`){let n=this.pending.indexOf(t);this.pending.splice(n,1),t.state=`settled`,t.removeAbortListener?.(),t.controller.abort(),t.resolve(e(t));return}let n=e(t);this.releaseActive(t),t.controller.abort(),t.resolve(n),this.startNext()}releaseActive(e){e.state=`settled`,e.removeAbortListener?.(),this.active=void 0}};exports.QuestionnaireCoordinator=t;
2
2
  //# sourceMappingURL=questionnaireCoordinator.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"questionnaireCoordinator.cjs","names":[],"sources":["../../src/services/questionnaireCoordinator.ts"],"sourcesContent":["import type { QuestionnaireResult } from '../types/questionnaire.js';\n\nexport interface QuestionnaireRunContext {\n signal: AbortSignal;\n reportProgress: (result: QuestionnaireResult) => void;\n}\n\nexport type QuestionnaireRunner = (context: QuestionnaireRunContext) => Promise<QuestionnaireResult>;\n\ninterface QueueEntry {\n id: number;\n runner: QuestionnaireRunner;\n controller: AbortController;\n externalSignal?: AbortSignal;\n removeAbortListener?: () => void;\n latestAnswers: QuestionnaireResult['answers'];\n state: 'queued' | 'active' | 'settled';\n resolve(result: QuestionnaireResult): void;\n reject(error: unknown): void;\n}\n\nfunction cancelledResult(entry: QueueEntry): QuestionnaireResult {\n return {\n answers: entry.latestAnswers.map((answer) => ({ ...answer })),\n cancelled: true,\n };\n}\n\nexport class QuestionnaireCoordinator {\n private readonly pending: QueueEntry[] = [];\n private active: QueueEntry | undefined;\n private nextRequestId = 1;\n private accepting = true;\n\n enqueue(runner: QuestionnaireRunner, externalSignal?: AbortSignal): Promise<QuestionnaireResult> {\n if (!this.accepting || externalSignal?.aborted) {\n return Promise.resolve({ answers: [], cancelled: true });\n }\n\n return new Promise<QuestionnaireResult>((resolve, reject) => {\n const entry: QueueEntry = {\n id: this.nextRequestId,\n runner,\n controller: new AbortController(),\n ...(externalSignal ? { externalSignal } : {}),\n latestAnswers: [],\n state: 'queued',\n resolve,\n reject,\n };\n this.nextRequestId += 1;\n const abort = (): void => this.cancel(entry);\n externalSignal?.addEventListener('abort', abort, { once: true });\n entry.removeAbortListener = () => externalSignal?.removeEventListener('abort', abort);\n this.pending.push(entry);\n this.startNext();\n });\n }\n\n shutdown(): void {\n if (!this.accepting && !this.active && this.pending.length === 0) return;\n this.accepting = false;\n if (this.active) this.cancel(this.active);\n while (this.pending.length > 0) {\n const entry = this.pending[0];\n if (!entry) break;\n this.cancel(entry);\n }\n }\n\n private startNext(): void {\n if (!this.accepting || this.active) return;\n const entry = this.pending.shift();\n if (!entry) return;\n entry.state = 'active';\n this.active = entry;\n const reportProgress = (result: QuestionnaireResult): void => {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n entry.latestAnswers = result.answers.map((answer) => ({ ...answer }));\n };\n void entry.runner({ signal: entry.controller.signal, reportProgress }).then(\n (result) => this.complete(entry, result),\n (error: unknown) => this.fail(entry, error),\n );\n }\n\n private complete(entry: QueueEntry, result: QuestionnaireResult): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.resolve(result);\n this.startNext();\n }\n\n private fail(entry: QueueEntry, error: unknown): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.reject(error);\n this.startNext();\n }\n\n private cancel(entry: QueueEntry): void {\n if (entry.state === 'settled') return;\n if (entry.state === 'queued') {\n const index = this.pending.indexOf(entry);\n this.pending.splice(index, 1);\n entry.state = 'settled';\n entry.removeAbortListener?.();\n entry.controller.abort();\n entry.resolve(cancelledResult(entry));\n return;\n }\n\n const result = cancelledResult(entry);\n this.releaseActive(entry);\n entry.controller.abort();\n entry.resolve(result);\n this.startNext();\n }\n\n private releaseActive(entry: QueueEntry): void {\n entry.state = 'settled';\n entry.removeAbortListener?.();\n this.active = undefined;\n }\n}\n"],"mappings":"AAqBA,SAAS,EAAgB,EAAwC,CAC/D,MAAO,CACL,QAAS,EAAM,cAAc,IAAK,IAAY,CAAE,GAAG,CAAO,EAAE,EAC5D,UAAW,EACb,CACF,CAEA,IAAa,EAAb,KAAsC,CACpC,QAAyC,CAAC,EAC1C,OACA,cAAwB,EACxB,UAAoB,GAEpB,QAAQ,EAA6B,EAA4D,CAK/F,MAJI,CAAC,KAAK,WAAa,GAAgB,QAC9B,QAAQ,QAAQ,CAAE,QAAS,CAAC,EAAG,UAAW,EAAK,CAAC,EAGlD,IAAI,SAA8B,EAAS,IAAW,CAC3D,IAAM,EAAoB,CACxB,GAAI,KAAK,cACT,SACA,WAAY,IAAI,gBAChB,GAAI,EAAiB,CAAE,gBAAe,EAAI,CAAC,EAC3C,cAAe,CAAC,EAChB,MAAO,SACP,UACA,QACF,EACA,KAAK,eAAiB,EACtB,IAAM,MAAoB,KAAK,OAAO,CAAK,EAC3C,GAAgB,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,EAC/D,EAAM,wBAA4B,GAAgB,oBAAoB,QAAS,CAAK,EACpF,KAAK,QAAQ,KAAK,CAAK,EACvB,KAAK,UAAU,CACjB,CAAC,CACH,CAEA,UAAiB,CACX,MAAC,KAAK,WAAa,CAAC,KAAK,QAAU,KAAK,QAAQ,SAAW,GAG/D,IAFA,KAAK,UAAY,GACb,KAAK,QAAQ,KAAK,OAAO,KAAK,MAAM,EACjC,KAAK,QAAQ,OAAS,GAAG,CAC9B,IAAM,EAAQ,KAAK,QAAQ,GAC3B,GAAI,CAAC,EAAO,MACZ,KAAK,OAAO,CAAK,CACnB,CACF,CAEA,WAA0B,CACxB,GAAI,CAAC,KAAK,WAAa,KAAK,OAAQ,OACpC,IAAM,EAAQ,KAAK,QAAQ,MAAM,EAC5B,IACL,EAAM,MAAQ,SACd,KAAK,OAAS,EAKd,EAAW,OAAO,CAAE,OAAQ,EAAM,WAAW,OAAQ,eAJ7B,GAAsC,CACxD,KAAK,QAAQ,KAAO,EAAM,IAAM,EAAM,QAAU,WACpD,EAAM,cAAgB,EAAO,QAAQ,IAAK,IAAY,CAAE,GAAG,CAAO,EAAE,EACtE,CACoE,CAAC,CAAC,CAAC,KACpE,GAAW,KAAK,SAAS,EAAO,CAAM,EACtC,GAAmB,KAAK,KAAK,EAAO,CAAK,CAC5C,EACF,CAEA,SAAiB,EAAmB,EAAmC,CACjE,KAAK,QAAQ,KAAO,EAAM,IAAM,EAAM,QAAU,WACpD,KAAK,cAAc,CAAK,EACxB,EAAM,QAAQ,CAAM,EACpB,KAAK,UAAU,EACjB,CAEA,KAAa,EAAmB,EAAsB,CAChD,KAAK,QAAQ,KAAO,EAAM,IAAM,EAAM,QAAU,WACpD,KAAK,cAAc,CAAK,EACxB,EAAM,OAAO,CAAK,EAClB,KAAK,UAAU,EACjB,CAEA,OAAe,EAAyB,CACtC,GAAI,EAAM,QAAU,UAAW,OAC/B,GAAI,EAAM,QAAU,SAAU,CAC5B,IAAM,EAAQ,KAAK,QAAQ,QAAQ,CAAK,EACxC,KAAK,QAAQ,OAAO,EAAO,CAAC,EAC5B,EAAM,MAAQ,UACd,EAAM,sBAAsB,EAC5B,EAAM,WAAW,MAAM,EACvB,EAAM,QAAQ,EAAgB,CAAK,CAAC,EACpC,MACF,CAEA,IAAM,EAAS,EAAgB,CAAK,EACpC,KAAK,cAAc,CAAK,EACxB,EAAM,WAAW,MAAM,EACvB,EAAM,QAAQ,CAAM,EACpB,KAAK,UAAU,CACjB,CAEA,cAAsB,EAAyB,CAC7C,EAAM,MAAQ,UACd,EAAM,sBAAsB,EAC5B,KAAK,OAAS,IAAA,EAChB,CACF"}
1
+ {"version":3,"file":"questionnaireCoordinator.cjs","names":[],"sources":["../../src/services/questionnaireCoordinator.ts"],"sourcesContent":["import type { QuestionnaireResult } from '../types/questionnaire.js';\n\nexport interface QuestionnaireRunContext {\n signal: AbortSignal;\n reportProgress: (result: QuestionnaireResult) => void;\n}\n\nexport type QuestionnaireRunner = (context: QuestionnaireRunContext) => Promise<QuestionnaireResult>;\n\ninterface QueueEntry {\n id: number;\n runner: QuestionnaireRunner;\n controller: AbortController;\n externalSignal?: AbortSignal;\n removeAbortListener?: () => void;\n latestAnswers: QuestionnaireResult['answers'];\n state: 'queued' | 'active' | 'settled';\n resolve(result: QuestionnaireResult): void;\n reject(error: unknown): void;\n}\n\nfunction cancelledResult(entry: QueueEntry): QuestionnaireResult {\n return {\n answers: entry.latestAnswers.map((answer) => ({ ...answer })),\n cancelled: true,\n };\n}\n\nexport class QuestionnaireCoordinator {\n private readonly pending: QueueEntry[] = [];\n private readonly running = new Set<Promise<QuestionnaireResult>>();\n private active: QueueEntry | undefined;\n private nextRequestId = 1;\n private accepting = true;\n\n enqueue(runner: QuestionnaireRunner, externalSignal?: AbortSignal): Promise<QuestionnaireResult> {\n if (!this.accepting || externalSignal?.aborted) {\n return Promise.resolve({ answers: [], cancelled: true });\n }\n\n return new Promise<QuestionnaireResult>((resolve, reject) => {\n const entry: QueueEntry = {\n id: this.nextRequestId,\n runner,\n controller: new AbortController(),\n ...(externalSignal ? { externalSignal } : {}),\n latestAnswers: [],\n state: 'queued',\n resolve,\n reject,\n };\n this.nextRequestId += 1;\n const abort = (): void => this.cancel(entry);\n externalSignal?.addEventListener('abort', abort, { once: true });\n entry.removeAbortListener = () => externalSignal?.removeEventListener('abort', abort);\n this.pending.push(entry);\n this.startNext();\n });\n }\n\n shutdown(): void {\n if (!this.accepting && !this.active && this.pending.length === 0) return;\n this.accepting = false;\n if (this.active) this.cancel(this.active);\n while (this.pending.length > 0) {\n const entry = this.pending[0];\n if (!entry) break;\n this.cancel(entry);\n }\n }\n\n /** Waits for every started presenter to observe cancellation and finish. */\n async waitForIdle(): Promise<void> {\n while (this.running.size > 0) await Promise.allSettled(this.running);\n }\n\n private startNext(): void {\n if (!this.accepting || this.active) return;\n const entry = this.pending.shift();\n if (!entry) return;\n entry.state = 'active';\n this.active = entry;\n const reportProgress = (result: QuestionnaireResult): void => {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n entry.latestAnswers = result.answers.map((answer) => ({ ...answer }));\n };\n let operation: Promise<QuestionnaireResult>;\n try {\n operation = entry.runner({ signal: entry.controller.signal, reportProgress });\n } catch (error) {\n operation = Promise.reject(error);\n }\n this.running.add(operation);\n void operation\n .then(\n (result) => this.complete(entry, result),\n (error: unknown) => this.fail(entry, error),\n )\n .finally(() => this.running.delete(operation));\n }\n\n private complete(entry: QueueEntry, result: QuestionnaireResult): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.resolve(result);\n this.startNext();\n }\n\n private fail(entry: QueueEntry, error: unknown): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.reject(error);\n this.startNext();\n }\n\n private cancel(entry: QueueEntry): void {\n if (entry.state === 'settled') return;\n if (entry.state === 'queued') {\n const index = this.pending.indexOf(entry);\n this.pending.splice(index, 1);\n entry.state = 'settled';\n entry.removeAbortListener?.();\n entry.controller.abort();\n entry.resolve(cancelledResult(entry));\n return;\n }\n\n const result = cancelledResult(entry);\n this.releaseActive(entry);\n entry.controller.abort();\n entry.resolve(result);\n this.startNext();\n }\n\n private releaseActive(entry: QueueEntry): void {\n entry.state = 'settled';\n entry.removeAbortListener?.();\n this.active = undefined;\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- function e(e){return{answers:e.latestAnswers.map(e=>({...e})),cancelled:!0}}var t=class{pending=[];active;nextRequestId=1;accepting=!0;enqueue(e,t){return!this.accepting||t?.aborted?Promise.resolve({answers:[],cancelled:!0}):new Promise((n,r)=>{let i={id:this.nextRequestId,runner:e,controller:new AbortController,...t?{externalSignal:t}:{},latestAnswers:[],state:`queued`,resolve:n,reject:r};this.nextRequestId+=1;let a=()=>this.cancel(i);t?.addEventListener(`abort`,a,{once:!0}),i.removeAbortListener=()=>t?.removeEventListener(`abort`,a),this.pending.push(i),this.startNext()})}shutdown(){if(!(!this.accepting&&!this.active&&this.pending.length===0))for(this.accepting=!1,this.active&&this.cancel(this.active);this.pending.length>0;){let e=this.pending[0];if(!e)break;this.cancel(e)}}startNext(){if(!this.accepting||this.active)return;let e=this.pending.shift();e&&(e.state=`active`,this.active=e,e.runner({signal:e.controller.signal,reportProgress:t=>{this.active?.id===e.id&&e.state===`active`&&(e.latestAnswers=t.answers.map(e=>({...e})))}}).then(t=>this.complete(e,t),t=>this.fail(e,t)))}complete(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.resolve(t),this.startNext())}fail(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.reject(t),this.startNext())}cancel(t){if(t.state===`settled`)return;if(t.state===`queued`){let n=this.pending.indexOf(t);this.pending.splice(n,1),t.state=`settled`,t.removeAbortListener?.(),t.controller.abort(),t.resolve(e(t));return}let n=e(t);this.releaseActive(t),t.controller.abort(),t.resolve(n),this.startNext()}releaseActive(e){e.state=`settled`,e.removeAbortListener?.(),this.active=void 0}};export{t as QuestionnaireCoordinator};
1
+ function e(e){return{answers:e.latestAnswers.map(e=>({...e})),cancelled:!0}}var t=class{pending=[];running=new Set;active;nextRequestId=1;accepting=!0;enqueue(e,t){return!this.accepting||t?.aborted?Promise.resolve({answers:[],cancelled:!0}):new Promise((n,r)=>{let i={id:this.nextRequestId,runner:e,controller:new AbortController,...t?{externalSignal:t}:{},latestAnswers:[],state:`queued`,resolve:n,reject:r};this.nextRequestId+=1;let a=()=>this.cancel(i);t?.addEventListener(`abort`,a,{once:!0}),i.removeAbortListener=()=>t?.removeEventListener(`abort`,a),this.pending.push(i),this.startNext()})}shutdown(){if(!(!this.accepting&&!this.active&&this.pending.length===0))for(this.accepting=!1,this.active&&this.cancel(this.active);this.pending.length>0;){let e=this.pending[0];if(!e)break;this.cancel(e)}}async waitForIdle(){for(;this.running.size>0;)await Promise.allSettled(this.running)}startNext(){if(!this.accepting||this.active)return;let e=this.pending.shift();if(!e)return;e.state=`active`,this.active=e;let t=t=>{this.active?.id===e.id&&e.state===`active`&&(e.latestAnswers=t.answers.map(e=>({...e})))},n;try{n=e.runner({signal:e.controller.signal,reportProgress:t})}catch(e){n=Promise.reject(e)}this.running.add(n),n.then(t=>this.complete(e,t),t=>this.fail(e,t)).finally(()=>this.running.delete(n))}complete(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.resolve(t),this.startNext())}fail(e,t){this.active?.id===e.id&&e.state===`active`&&(this.releaseActive(e),e.reject(t),this.startNext())}cancel(t){if(t.state===`settled`)return;if(t.state===`queued`){let n=this.pending.indexOf(t);this.pending.splice(n,1),t.state=`settled`,t.removeAbortListener?.(),t.controller.abort(),t.resolve(e(t));return}let n=e(t);this.releaseActive(t),t.controller.abort(),t.resolve(n),this.startNext()}releaseActive(e){e.state=`settled`,e.removeAbortListener?.(),this.active=void 0}};export{t as QuestionnaireCoordinator};
2
2
  //# sourceMappingURL=questionnaireCoordinator.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"questionnaireCoordinator.mjs","names":[],"sources":["../../src/services/questionnaireCoordinator.ts"],"sourcesContent":["import type { QuestionnaireResult } from '../types/questionnaire.js';\n\nexport interface QuestionnaireRunContext {\n signal: AbortSignal;\n reportProgress: (result: QuestionnaireResult) => void;\n}\n\nexport type QuestionnaireRunner = (context: QuestionnaireRunContext) => Promise<QuestionnaireResult>;\n\ninterface QueueEntry {\n id: number;\n runner: QuestionnaireRunner;\n controller: AbortController;\n externalSignal?: AbortSignal;\n removeAbortListener?: () => void;\n latestAnswers: QuestionnaireResult['answers'];\n state: 'queued' | 'active' | 'settled';\n resolve(result: QuestionnaireResult): void;\n reject(error: unknown): void;\n}\n\nfunction cancelledResult(entry: QueueEntry): QuestionnaireResult {\n return {\n answers: entry.latestAnswers.map((answer) => ({ ...answer })),\n cancelled: true,\n };\n}\n\nexport class QuestionnaireCoordinator {\n private readonly pending: QueueEntry[] = [];\n private active: QueueEntry | undefined;\n private nextRequestId = 1;\n private accepting = true;\n\n enqueue(runner: QuestionnaireRunner, externalSignal?: AbortSignal): Promise<QuestionnaireResult> {\n if (!this.accepting || externalSignal?.aborted) {\n return Promise.resolve({ answers: [], cancelled: true });\n }\n\n return new Promise<QuestionnaireResult>((resolve, reject) => {\n const entry: QueueEntry = {\n id: this.nextRequestId,\n runner,\n controller: new AbortController(),\n ...(externalSignal ? { externalSignal } : {}),\n latestAnswers: [],\n state: 'queued',\n resolve,\n reject,\n };\n this.nextRequestId += 1;\n const abort = (): void => this.cancel(entry);\n externalSignal?.addEventListener('abort', abort, { once: true });\n entry.removeAbortListener = () => externalSignal?.removeEventListener('abort', abort);\n this.pending.push(entry);\n this.startNext();\n });\n }\n\n shutdown(): void {\n if (!this.accepting && !this.active && this.pending.length === 0) return;\n this.accepting = false;\n if (this.active) this.cancel(this.active);\n while (this.pending.length > 0) {\n const entry = this.pending[0];\n if (!entry) break;\n this.cancel(entry);\n }\n }\n\n private startNext(): void {\n if (!this.accepting || this.active) return;\n const entry = this.pending.shift();\n if (!entry) return;\n entry.state = 'active';\n this.active = entry;\n const reportProgress = (result: QuestionnaireResult): void => {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n entry.latestAnswers = result.answers.map((answer) => ({ ...answer }));\n };\n void entry.runner({ signal: entry.controller.signal, reportProgress }).then(\n (result) => this.complete(entry, result),\n (error: unknown) => this.fail(entry, error),\n );\n }\n\n private complete(entry: QueueEntry, result: QuestionnaireResult): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.resolve(result);\n this.startNext();\n }\n\n private fail(entry: QueueEntry, error: unknown): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.reject(error);\n this.startNext();\n }\n\n private cancel(entry: QueueEntry): void {\n if (entry.state === 'settled') return;\n if (entry.state === 'queued') {\n const index = this.pending.indexOf(entry);\n this.pending.splice(index, 1);\n entry.state = 'settled';\n entry.removeAbortListener?.();\n entry.controller.abort();\n entry.resolve(cancelledResult(entry));\n return;\n }\n\n const result = cancelledResult(entry);\n this.releaseActive(entry);\n entry.controller.abort();\n entry.resolve(result);\n this.startNext();\n }\n\n private releaseActive(entry: QueueEntry): void {\n entry.state = 'settled';\n entry.removeAbortListener?.();\n this.active = undefined;\n }\n}\n"],"mappings":"AAqBA,SAAS,EAAgB,EAAwC,CAC/D,MAAO,CACL,QAAS,EAAM,cAAc,IAAK,IAAY,CAAE,GAAG,CAAO,EAAE,EAC5D,UAAW,EACb,CACF,CAEA,IAAa,EAAb,KAAsC,CACpC,QAAyC,CAAC,EAC1C,OACA,cAAwB,EACxB,UAAoB,GAEpB,QAAQ,EAA6B,EAA4D,CAK/F,MAJI,CAAC,KAAK,WAAa,GAAgB,QAC9B,QAAQ,QAAQ,CAAE,QAAS,CAAC,EAAG,UAAW,EAAK,CAAC,EAGlD,IAAI,SAA8B,EAAS,IAAW,CAC3D,IAAM,EAAoB,CACxB,GAAI,KAAK,cACT,SACA,WAAY,IAAI,gBAChB,GAAI,EAAiB,CAAE,gBAAe,EAAI,CAAC,EAC3C,cAAe,CAAC,EAChB,MAAO,SACP,UACA,QACF,EACA,KAAK,eAAiB,EACtB,IAAM,MAAoB,KAAK,OAAO,CAAK,EAC3C,GAAgB,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,EAC/D,EAAM,wBAA4B,GAAgB,oBAAoB,QAAS,CAAK,EACpF,KAAK,QAAQ,KAAK,CAAK,EACvB,KAAK,UAAU,CACjB,CAAC,CACH,CAEA,UAAiB,CACX,MAAC,KAAK,WAAa,CAAC,KAAK,QAAU,KAAK,QAAQ,SAAW,GAG/D,IAFA,KAAK,UAAY,GACb,KAAK,QAAQ,KAAK,OAAO,KAAK,MAAM,EACjC,KAAK,QAAQ,OAAS,GAAG,CAC9B,IAAM,EAAQ,KAAK,QAAQ,GAC3B,GAAI,CAAC,EAAO,MACZ,KAAK,OAAO,CAAK,CACnB,CACF,CAEA,WAA0B,CACxB,GAAI,CAAC,KAAK,WAAa,KAAK,OAAQ,OACpC,IAAM,EAAQ,KAAK,QAAQ,MAAM,EAC5B,IACL,EAAM,MAAQ,SACd,KAAK,OAAS,EAKd,EAAW,OAAO,CAAE,OAAQ,EAAM,WAAW,OAAQ,eAJ7B,GAAsC,CACxD,KAAK,QAAQ,KAAO,EAAM,IAAM,EAAM,QAAU,WACpD,EAAM,cAAgB,EAAO,QAAQ,IAAK,IAAY,CAAE,GAAG,CAAO,EAAE,EACtE,CACoE,CAAC,CAAC,CAAC,KACpE,GAAW,KAAK,SAAS,EAAO,CAAM,EACtC,GAAmB,KAAK,KAAK,EAAO,CAAK,CAC5C,EACF,CAEA,SAAiB,EAAmB,EAAmC,CACjE,KAAK,QAAQ,KAAO,EAAM,IAAM,EAAM,QAAU,WACpD,KAAK,cAAc,CAAK,EACxB,EAAM,QAAQ,CAAM,EACpB,KAAK,UAAU,EACjB,CAEA,KAAa,EAAmB,EAAsB,CAChD,KAAK,QAAQ,KAAO,EAAM,IAAM,EAAM,QAAU,WACpD,KAAK,cAAc,CAAK,EACxB,EAAM,OAAO,CAAK,EAClB,KAAK,UAAU,EACjB,CAEA,OAAe,EAAyB,CACtC,GAAI,EAAM,QAAU,UAAW,OAC/B,GAAI,EAAM,QAAU,SAAU,CAC5B,IAAM,EAAQ,KAAK,QAAQ,QAAQ,CAAK,EACxC,KAAK,QAAQ,OAAO,EAAO,CAAC,EAC5B,EAAM,MAAQ,UACd,EAAM,sBAAsB,EAC5B,EAAM,WAAW,MAAM,EACvB,EAAM,QAAQ,EAAgB,CAAK,CAAC,EACpC,MACF,CAEA,IAAM,EAAS,EAAgB,CAAK,EACpC,KAAK,cAAc,CAAK,EACxB,EAAM,WAAW,MAAM,EACvB,EAAM,QAAQ,CAAM,EACpB,KAAK,UAAU,CACjB,CAEA,cAAsB,EAAyB,CAC7C,EAAM,MAAQ,UACd,EAAM,sBAAsB,EAC5B,KAAK,OAAS,IAAA,EAChB,CACF"}
1
+ {"version":3,"file":"questionnaireCoordinator.mjs","names":[],"sources":["../../src/services/questionnaireCoordinator.ts"],"sourcesContent":["import type { QuestionnaireResult } from '../types/questionnaire.js';\n\nexport interface QuestionnaireRunContext {\n signal: AbortSignal;\n reportProgress: (result: QuestionnaireResult) => void;\n}\n\nexport type QuestionnaireRunner = (context: QuestionnaireRunContext) => Promise<QuestionnaireResult>;\n\ninterface QueueEntry {\n id: number;\n runner: QuestionnaireRunner;\n controller: AbortController;\n externalSignal?: AbortSignal;\n removeAbortListener?: () => void;\n latestAnswers: QuestionnaireResult['answers'];\n state: 'queued' | 'active' | 'settled';\n resolve(result: QuestionnaireResult): void;\n reject(error: unknown): void;\n}\n\nfunction cancelledResult(entry: QueueEntry): QuestionnaireResult {\n return {\n answers: entry.latestAnswers.map((answer) => ({ ...answer })),\n cancelled: true,\n };\n}\n\nexport class QuestionnaireCoordinator {\n private readonly pending: QueueEntry[] = [];\n private readonly running = new Set<Promise<QuestionnaireResult>>();\n private active: QueueEntry | undefined;\n private nextRequestId = 1;\n private accepting = true;\n\n enqueue(runner: QuestionnaireRunner, externalSignal?: AbortSignal): Promise<QuestionnaireResult> {\n if (!this.accepting || externalSignal?.aborted) {\n return Promise.resolve({ answers: [], cancelled: true });\n }\n\n return new Promise<QuestionnaireResult>((resolve, reject) => {\n const entry: QueueEntry = {\n id: this.nextRequestId,\n runner,\n controller: new AbortController(),\n ...(externalSignal ? { externalSignal } : {}),\n latestAnswers: [],\n state: 'queued',\n resolve,\n reject,\n };\n this.nextRequestId += 1;\n const abort = (): void => this.cancel(entry);\n externalSignal?.addEventListener('abort', abort, { once: true });\n entry.removeAbortListener = () => externalSignal?.removeEventListener('abort', abort);\n this.pending.push(entry);\n this.startNext();\n });\n }\n\n shutdown(): void {\n if (!this.accepting && !this.active && this.pending.length === 0) return;\n this.accepting = false;\n if (this.active) this.cancel(this.active);\n while (this.pending.length > 0) {\n const entry = this.pending[0];\n if (!entry) break;\n this.cancel(entry);\n }\n }\n\n /** Waits for every started presenter to observe cancellation and finish. */\n async waitForIdle(): Promise<void> {\n while (this.running.size > 0) await Promise.allSettled(this.running);\n }\n\n private startNext(): void {\n if (!this.accepting || this.active) return;\n const entry = this.pending.shift();\n if (!entry) return;\n entry.state = 'active';\n this.active = entry;\n const reportProgress = (result: QuestionnaireResult): void => {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n entry.latestAnswers = result.answers.map((answer) => ({ ...answer }));\n };\n let operation: Promise<QuestionnaireResult>;\n try {\n operation = entry.runner({ signal: entry.controller.signal, reportProgress });\n } catch (error) {\n operation = Promise.reject(error);\n }\n this.running.add(operation);\n void operation\n .then(\n (result) => this.complete(entry, result),\n (error: unknown) => this.fail(entry, error),\n )\n .finally(() => this.running.delete(operation));\n }\n\n private complete(entry: QueueEntry, result: QuestionnaireResult): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.resolve(result);\n this.startNext();\n }\n\n private fail(entry: QueueEntry, error: unknown): void {\n if (this.active?.id !== entry.id || entry.state !== 'active') return;\n this.releaseActive(entry);\n entry.reject(error);\n this.startNext();\n }\n\n private cancel(entry: QueueEntry): void {\n if (entry.state === 'settled') return;\n if (entry.state === 'queued') {\n const index = this.pending.indexOf(entry);\n this.pending.splice(index, 1);\n entry.state = 'settled';\n entry.removeAbortListener?.();\n entry.controller.abort();\n entry.resolve(cancelledResult(entry));\n return;\n }\n\n const result = cancelledResult(entry);\n this.releaseActive(entry);\n entry.controller.abort();\n entry.resolve(result);\n this.startNext();\n }\n\n private releaseActive(entry: QueueEntry): void {\n entry.state = 'settled';\n entry.removeAbortListener?.();\n this.active = undefined;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ //#region src/types/questionnaire.d.ts
2
+ type AnswerKind = 'option' | 'custom' | 'multi';
3
+ interface QuestionAnswer {
4
+ questionIndex: number;
5
+ question: string;
6
+ kind: AnswerKind;
7
+ answer: string | null;
8
+ selected?: string[];
9
+ notes?: string;
10
+ preview?: string;
11
+ }
12
+ type QuestionnaireError = 'no_ui' | 'no_custom_ui' | 'no_questions' | 'empty_options' | 'too_many_questions' | 'duplicate_question' | 'duplicate_option_label' | 'reserved_label' | 'session_load_failed' | 'stale_module_cache';
13
+ interface QuestionnaireResult {
14
+ answers: QuestionAnswer[];
15
+ cancelled: boolean;
16
+ error?: QuestionnaireError;
17
+ delivery?: 'voice';
18
+ awaitingResponse?: boolean;
19
+ voicePrompt?: string;
20
+ }
21
+ interface ToolTextResult {
22
+ content: Array<{
23
+ type: 'text';
24
+ text: string;
25
+ }>;
26
+ details: QuestionnaireResult;
27
+ terminate?: boolean;
28
+ }
29
+ //#endregion
30
+ export { AnswerKind, QuestionAnswer, QuestionnaireError, QuestionnaireResult, ToolTextResult };
31
+ //# sourceMappingURL=questionnaire.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questionnaire.d.cts","names":[],"sources":["../../src/types/questionnaire.ts"],"mappings":";KAAY;UAEK;EACf;EACA;EACA,MAAM;EACN;EACA;EACA;EACA;;KAGU;UAgBK;EACf,SAAS;EACT;EACA,QAAQ;EACR;EACA;EACA;;UAGe;EACf,SAAS;IAAQ;IAAc;;EAC/B,SAAS;EACT"}
@@ -0,0 +1,31 @@
1
+ //#region src/types/questionnaire.d.ts
2
+ type AnswerKind = 'option' | 'custom' | 'multi';
3
+ interface QuestionAnswer {
4
+ questionIndex: number;
5
+ question: string;
6
+ kind: AnswerKind;
7
+ answer: string | null;
8
+ selected?: string[];
9
+ notes?: string;
10
+ preview?: string;
11
+ }
12
+ type QuestionnaireError = 'no_ui' | 'no_custom_ui' | 'no_questions' | 'empty_options' | 'too_many_questions' | 'duplicate_question' | 'duplicate_option_label' | 'reserved_label' | 'session_load_failed' | 'stale_module_cache';
13
+ interface QuestionnaireResult {
14
+ answers: QuestionAnswer[];
15
+ cancelled: boolean;
16
+ error?: QuestionnaireError;
17
+ delivery?: 'voice';
18
+ awaitingResponse?: boolean;
19
+ voicePrompt?: string;
20
+ }
21
+ interface ToolTextResult {
22
+ content: Array<{
23
+ type: 'text';
24
+ text: string;
25
+ }>;
26
+ details: QuestionnaireResult;
27
+ terminate?: boolean;
28
+ }
29
+ //#endregion
30
+ export { AnswerKind, QuestionAnswer, QuestionnaireError, QuestionnaireResult, ToolTextResult };
31
+ //# sourceMappingURL=questionnaire.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questionnaire.d.mts","names":[],"sources":["../../src/types/questionnaire.ts"],"mappings":";KAAY;UAEK;EACf;EACA;EACA,MAAM;EACN;EACA;EACA;EACA;;KAGU;UAgBK;EACf,SAAS;EACT;EACA,QAAQ;EACR;EACA;EACA;;UAGe;EACf,SAAS;IAAQ;IAAc;;EAC/B,SAAS;EACT"}
package/package.json CHANGED
@@ -1,7 +1,19 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-user-feedback",
3
- "version": "0.0.1-alpha.2",
4
- "description": "Structured user feedback for DoomPi agents",
3
+ "version": "0.0.1-alpha.20",
4
+ "description": "Structured user questions with interactive and autonomous Voice handoff for Pi agents.",
5
+ "keywords": [
6
+ "ai",
7
+ "ask-user-question",
8
+ "coding-agent",
9
+ "developer-tools",
10
+ "doompi",
11
+ "human-in-the-loop",
12
+ "pi-coding-agent",
13
+ "pi-extension",
14
+ "pi-package",
15
+ "user-feedback"
16
+ ],
5
17
  "homepage": "https://agimon.ai",
6
18
  "license": "MIT",
7
19
  "author": "Vuong Ngo",
@@ -21,11 +33,6 @@
21
33
  "import": "./dist/index.mjs",
22
34
  "require": "./dist/index.cjs"
23
35
  },
24
- "./events": {
25
- "types": "./dist/events.d.mts",
26
- "import": "./dist/events.mjs",
27
- "require": "./dist/events.cjs"
28
- },
29
36
  "./extensions/pi": {
30
37
  "types": "./dist/extensions/pi.d.mts",
31
38
  "import": "./dist/extensions/pi.mjs",
@@ -37,22 +44,23 @@
37
44
  "access": "public"
38
45
  },
39
46
  "dependencies": {
40
- "typebox": "1.1.38",
41
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.13",
42
- "@agimon-ai/doompi-ui": "0.0.1-alpha.13"
47
+ "@deepseek-ai/cordis": "4.0.1",
48
+ "typebox": "1.3.16",
49
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.31",
50
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.31"
43
51
  },
44
52
  "devDependencies": {
45
- "@earendil-works/pi-coding-agent": "0.84.1",
46
- "@earendil-works/pi-tui": "0.84.1",
47
- "@types/node": "26.1.1",
48
- "@vitest/coverage-v8": "4.1.10",
53
+ "@earendil-works/pi-coding-agent": "0.84.2",
54
+ "@earendil-works/pi-tui": "0.84.2",
55
+ "@types/node": "26.2.0",
56
+ "@vitest/coverage-v8": "4.1.11",
49
57
  "tsdown": "0.22.14",
50
- "typescript": "6.0.3",
51
- "vitest": "4.1.10"
58
+ "typescript": "7.0.2",
59
+ "vitest": "4.1.11"
52
60
  },
53
61
  "peerDependencies": {
54
- "@earendil-works/pi-coding-agent": "0.84.1",
55
- "@earendil-works/pi-tui": "0.84.1"
62
+ "@earendil-works/pi-coding-agent": "0.84.2",
63
+ "@earendil-works/pi-tui": "0.84.2"
56
64
  },
57
65
  "peerDependenciesMeta": {
58
66
  "@earendil-works/pi-coding-agent": {
package/dist/events.cjs DELETED
@@ -1 +0,0 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require("./schemas/events.cjs");let e=require("@agimon-ai/doompi-extension-contracts");Object.defineProperty(exports,"ASK_USER_BLOCKED_EVENT",{enumerable:!0,get:function(){return e.ASK_USER_BLOCKED_EVENT}}),Object.defineProperty(exports,"ASK_USER_PROMPT_EVENT",{enumerable:!0,get:function(){return e.ASK_USER_PROMPT_EVENT}});
package/dist/events.d.cts DELETED
@@ -1,2 +0,0 @@
1
- import { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, AskUserBlockedEventPayload, AskUserPromptEventPayload, AskUserPromptOption, AskUserPromptQuestion } from "./schemas/events.cjs";
2
- export { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, type AskUserBlockedEventPayload, type AskUserPromptEventPayload, type AskUserPromptOption, type AskUserPromptQuestion };
package/dist/events.d.mts DELETED
@@ -1,2 +0,0 @@
1
- import { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, AskUserBlockedEventPayload, AskUserPromptEventPayload, AskUserPromptOption, AskUserPromptQuestion } from "./schemas/events.mjs";
2
- export { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, type AskUserBlockedEventPayload, type AskUserPromptEventPayload, type AskUserPromptOption, type AskUserPromptQuestion };
package/dist/events.mjs DELETED
@@ -1 +0,0 @@
1
- import{ASK_USER_BLOCKED_EVENT as e,ASK_USER_PROMPT_EVENT as t}from"./schemas/events.mjs";export{e as ASK_USER_BLOCKED_EVENT,t as ASK_USER_PROMPT_EVENT};
@@ -1 +0,0 @@
1
- {"version":3,"file":"pi.cjs","names":["QuestionnaireCoordinator","DoomModeStatusRegistry","createProtocolRuntime","VoiceQuestionHandoff","createNarrationRequester","buildVoiceQuestionNarration"],"sources":["../../src/extensions/pi.ts"],"sourcesContent":["import { DoomModeStatusRegistry } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { createNarrationRequester } from '@agimon-ai/doompi-extension-contracts/narration';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { VoiceQuestionHandoff } from '../adapters/doom/voiceQuestionHandoff.js';\nimport { registerAskUserQuestionTool } from '../adapters/pi/askUserQuestionAdapter.js';\nimport { registerAskUserQuestionReconciler } from '../adapters/pi/reconcileAdapter.js';\nimport { QuestionnaireCoordinator } from '../services/questionnaireCoordinator.js';\nimport { buildVoiceQuestionNarration } from '../services/voiceQuestionNarration.js';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-user-feedback';\nconst INSTALLATIONS_KEY = Symbol.for(`${PACKAGE_SOURCE}/installed-hosts`);\n\ntype InstallationRegistry = typeof globalThis & {\n [INSTALLATIONS_KEY]?: WeakSet<object>;\n};\n\nfunction claimHostInstallation(pi: ExtensionAPI): boolean {\n const registry = globalThis as InstallationRegistry;\n const installedHosts = registry[INSTALLATIONS_KEY] ?? new WeakSet<object>();\n registry[INSTALLATIONS_KEY] = installedHosts;\n if (installedHosts.has(pi)) return false;\n installedHosts.add(pi);\n return true;\n}\n\nexport function registerUserFeedbackExtension(pi: ExtensionAPI): void {\n if (!claimHostInstallation(pi)) return;\n\n let coordinator = new QuestionnaireCoordinator();\n let modes: DoomModeStatusRegistry | undefined;\n let disposeModes: (() => void) | undefined;\n let voiceHandoff: VoiceQuestionHandoff | undefined;\n const resetVoiceHandoff = (): void => {\n disposeModes?.();\n disposeModes = undefined;\n modes?.reset();\n modes = undefined;\n voiceHandoff = undefined;\n };\n\n registerAskUserQuestionTool(pi, {\n enqueue: (runner, signal) => coordinator.enqueue(runner, signal),\n runTui: async (...args) => {\n const { runTuiQuestionnaire } = await import('../tui/runQuestionnaire.js');\n return runTuiQuestionnaire(...args);\n },\n tryVoice: (params) => voiceHandoff?.handoff(params),\n });\n registerAskUserQuestionReconciler(pi);\n\n pi.on('session_start', (_event, context) => {\n coordinator.shutdown();\n coordinator = new QuestionnaireCoordinator();\n resetVoiceHandoff();\n\n const sessionId = context.sessionManager.getSessionId();\n const registry = new DoomModeStatusRegistry();\n const runtime = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId,\n });\n modes = registry;\n disposeModes = registry.bind(runtime);\n voiceHandoff = new VoiceQuestionHandoff(\n registry,\n createNarrationRequester({ bus: pi.events, source: PACKAGE_SOURCE, sessionId }),\n buildVoiceQuestionNarration,\n );\n });\n pi.on('session_shutdown', () => {\n coordinator.shutdown();\n resetVoiceHandoff();\n });\n}\n\nexport { registerUserFeedbackExtension as default };\n"],"mappings":"miBAUA,MAAM,EAAiB,kCACjB,EAAoB,OAAO,IAAI,GAAG,EAAe,iBAAiB,EAMxE,SAAS,EAAsB,EAA2B,CACxD,IAAM,EAAW,WACX,EAAiB,EAAS,IAAsB,IAAI,QAI1D,MAHA,GAAS,GAAqB,EAC9B,CAAI,EAAe,IAAI,CAAE,IACzB,EAAe,IAAI,CAAE,EACd,GACT,CAEA,SAAgB,EAA8B,EAAwB,CACpE,GAAI,CAAC,EAAsB,CAAE,EAAG,OAEhC,IAAI,EAAc,IAAIA,EAAAA,yBAClB,EACA,EACA,EACE,MAAgC,CACpC,IAAe,EACf,EAAe,IAAA,GACf,GAAO,MAAM,EACb,EAAQ,IAAA,GACR,EAAe,IAAA,EACjB,EAEA,EAAA,4BAA4B,EAAI,CAC9B,SAAU,EAAQ,IAAW,EAAY,QAAQ,EAAQ,CAAM,EAC/D,OAAQ,MAAO,GAAG,IAAS,CACzB,GAAM,CAAE,uBAAwB,MAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAM,6BAAA,CAAA,EACtC,OAAO,EAAoB,GAAG,CAAI,CACpC,EACA,SAAW,GAAW,GAAc,QAAQ,CAAM,CACpD,CAAC,EACD,EAAA,kCAAkC,CAAE,EAEpC,EAAG,GAAG,iBAAkB,EAAQ,IAAY,CAC1C,EAAY,SAAS,EACrB,EAAc,IAAIA,EAAAA,yBAClB,EAAkB,EAElB,IAAM,EAAY,EAAQ,eAAe,aAAa,EAChD,EAAW,IAAIC,EAAAA,uBACf,GAAA,EAAUC,EAAAA,sBAAAA,CAAsB,CACpC,IAAK,EAAG,OACR,OAAQ,EACR,WACF,CAAC,EACD,EAAQ,EACR,EAAe,EAAS,KAAK,CAAO,EACpC,EAAe,IAAIC,EAAAA,qBACjB,GAAA,EACAC,EAAAA,yBAAAA,CAAyB,CAAE,IAAK,EAAG,OAAQ,OAAQ,EAAgB,WAAU,CAAC,EAC9EC,EAAAA,2BACF,CACF,CAAC,EACD,EAAG,GAAG,uBAA0B,CAC9B,EAAY,SAAS,EACrB,EAAkB,CACpB,CAAC,CACH"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"pi.d.cts","names":[],"sources":["../../src/extensions/pi.ts"],"mappings":";;iBA0BgB,8BAA8B,IAAI"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"pi.d.mts","names":[],"sources":["../../src/extensions/pi.ts"],"mappings":";;iBA0BgB,8BAA8B,IAAI"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"pi.mjs","names":[],"sources":["../../src/extensions/pi.ts"],"sourcesContent":["import { DoomModeStatusRegistry } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { createNarrationRequester } from '@agimon-ai/doompi-extension-contracts/narration';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { VoiceQuestionHandoff } from '../adapters/doom/voiceQuestionHandoff.js';\nimport { registerAskUserQuestionTool } from '../adapters/pi/askUserQuestionAdapter.js';\nimport { registerAskUserQuestionReconciler } from '../adapters/pi/reconcileAdapter.js';\nimport { QuestionnaireCoordinator } from '../services/questionnaireCoordinator.js';\nimport { buildVoiceQuestionNarration } from '../services/voiceQuestionNarration.js';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-user-feedback';\nconst INSTALLATIONS_KEY = Symbol.for(`${PACKAGE_SOURCE}/installed-hosts`);\n\ntype InstallationRegistry = typeof globalThis & {\n [INSTALLATIONS_KEY]?: WeakSet<object>;\n};\n\nfunction claimHostInstallation(pi: ExtensionAPI): boolean {\n const registry = globalThis as InstallationRegistry;\n const installedHosts = registry[INSTALLATIONS_KEY] ?? new WeakSet<object>();\n registry[INSTALLATIONS_KEY] = installedHosts;\n if (installedHosts.has(pi)) return false;\n installedHosts.add(pi);\n return true;\n}\n\nexport function registerUserFeedbackExtension(pi: ExtensionAPI): void {\n if (!claimHostInstallation(pi)) return;\n\n let coordinator = new QuestionnaireCoordinator();\n let modes: DoomModeStatusRegistry | undefined;\n let disposeModes: (() => void) | undefined;\n let voiceHandoff: VoiceQuestionHandoff | undefined;\n const resetVoiceHandoff = (): void => {\n disposeModes?.();\n disposeModes = undefined;\n modes?.reset();\n modes = undefined;\n voiceHandoff = undefined;\n };\n\n registerAskUserQuestionTool(pi, {\n enqueue: (runner, signal) => coordinator.enqueue(runner, signal),\n runTui: async (...args) => {\n const { runTuiQuestionnaire } = await import('../tui/runQuestionnaire.js');\n return runTuiQuestionnaire(...args);\n },\n tryVoice: (params) => voiceHandoff?.handoff(params),\n });\n registerAskUserQuestionReconciler(pi);\n\n pi.on('session_start', (_event, context) => {\n coordinator.shutdown();\n coordinator = new QuestionnaireCoordinator();\n resetVoiceHandoff();\n\n const sessionId = context.sessionManager.getSessionId();\n const registry = new DoomModeStatusRegistry();\n const runtime = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId,\n });\n modes = registry;\n disposeModes = registry.bind(runtime);\n voiceHandoff = new VoiceQuestionHandoff(\n registry,\n createNarrationRequester({ bus: pi.events, source: PACKAGE_SOURCE, sessionId }),\n buildVoiceQuestionNarration,\n );\n });\n pi.on('session_shutdown', () => {\n coordinator.shutdown();\n resetVoiceHandoff();\n });\n}\n\nexport { registerUserFeedbackExtension as default };\n"],"mappings":"grBAUA,MAAM,EAAiB,kCACjB,EAAoB,OAAO,IAAI,GAAG,EAAe,iBAAiB,EAMxE,SAAS,EAAsB,EAA2B,CACxD,IAAM,EAAW,WACX,EAAiB,EAAS,IAAsB,IAAI,QAI1D,MAHA,GAAS,GAAqB,EAC9B,CAAI,EAAe,IAAI,CAAE,IACzB,EAAe,IAAI,CAAE,EACd,GACT,CAEA,SAAgB,EAA8B,EAAwB,CACpE,GAAI,CAAC,EAAsB,CAAE,EAAG,OAEhC,IAAI,EAAc,IAAI,EAClB,EACA,EACA,EACE,MAAgC,CACpC,IAAe,EACf,EAAe,IAAA,GACf,GAAO,MAAM,EACb,EAAQ,IAAA,GACR,EAAe,IAAA,EACjB,EAEA,EAA4B,EAAI,CAC9B,SAAU,EAAQ,IAAW,EAAY,QAAQ,EAAQ,CAAM,EAC/D,OAAQ,MAAO,GAAG,IAAS,CACzB,GAAM,CAAE,uBAAwB,MAAM,OAAO,+BAC7C,OAAO,EAAoB,GAAG,CAAI,CACpC,EACA,SAAW,GAAW,GAAc,QAAQ,CAAM,CACpD,CAAC,EACD,EAAkC,CAAE,EAEpC,EAAG,GAAG,iBAAkB,EAAQ,IAAY,CAC1C,EAAY,SAAS,EACrB,EAAc,IAAI,EAClB,EAAkB,EAElB,IAAM,EAAY,EAAQ,eAAe,aAAa,EAChD,EAAW,IAAI,EACf,EAAU,EAAsB,CACpC,IAAK,EAAG,OACR,OAAQ,EACR,WACF,CAAC,EACD,EAAQ,EACR,EAAe,EAAS,KAAK,CAAO,EACpC,EAAe,IAAI,EACjB,EACA,EAAyB,CAAE,IAAK,EAAG,OAAQ,OAAQ,EAAgB,WAAU,CAAC,EAC9E,CACF,CACF,CAAC,EACD,EAAG,GAAG,uBAA0B,CAC9B,EAAY,SAAS,EACrB,EAAkB,CACpB,CAAC,CACH"}
@@ -1 +0,0 @@
1
- require("@agimon-ai/doompi-extension-contracts");
@@ -1,22 +0,0 @@
1
- import { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT } from "@agimon-ai/doompi-extension-contracts";
2
- //#region src/schemas/events.d.ts
3
- interface AskUserPromptOption {
4
- label: string;
5
- description: string;
6
- hasPreview: boolean;
7
- }
8
- interface AskUserPromptQuestion {
9
- question: string;
10
- header: string;
11
- multiSelect: boolean;
12
- options: AskUserPromptOption[];
13
- }
14
- interface AskUserPromptEventPayload {
15
- questions: AskUserPromptQuestion[];
16
- }
17
- interface AskUserBlockedEventPayload {
18
- active: boolean;
19
- }
20
- //#endregion
21
- export { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, AskUserBlockedEventPayload, AskUserPromptEventPayload, AskUserPromptOption, AskUserPromptQuestion };
22
- //# sourceMappingURL=events.d.cts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"events.d.cts","names":[],"sources":["../../src/schemas/events.ts"],"mappings":";;UAEiB;EACf;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA,SAAS;;UAGM;EACf,WAAW;;UAGI;EACf"}
@@ -1,22 +0,0 @@
1
- import { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT } from "@agimon-ai/doompi-extension-contracts";
2
- //#region src/schemas/events.d.ts
3
- interface AskUserPromptOption {
4
- label: string;
5
- description: string;
6
- hasPreview: boolean;
7
- }
8
- interface AskUserPromptQuestion {
9
- question: string;
10
- header: string;
11
- multiSelect: boolean;
12
- options: AskUserPromptOption[];
13
- }
14
- interface AskUserPromptEventPayload {
15
- questions: AskUserPromptQuestion[];
16
- }
17
- interface AskUserBlockedEventPayload {
18
- active: boolean;
19
- }
20
- //#endregion
21
- export { ASK_USER_BLOCKED_EVENT, ASK_USER_PROMPT_EVENT, AskUserBlockedEventPayload, AskUserPromptEventPayload, AskUserPromptOption, AskUserPromptQuestion };
22
- //# sourceMappingURL=events.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"events.d.mts","names":[],"sources":["../../src/schemas/events.ts"],"mappings":";;UAEiB;EACf;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA,SAAS;;UAGM;EACf,WAAW;;UAGI;EACf"}
@@ -1 +0,0 @@
1
- import{ASK_USER_BLOCKED_EVENT as e,ASK_USER_PROMPT_EVENT as t}from"@agimon-ai/doompi-extension-contracts";export{e as ASK_USER_BLOCKED_EVENT,t as ASK_USER_PROMPT_EVENT};