@agimon-ai/doompi-user-feedback 0.0.1-alpha.4 → 0.0.1-alpha.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -34
- package/dist/adapters/doom/voiceQuestionHandoff.cjs +2 -2
- package/dist/adapters/doom/voiceQuestionHandoff.cjs.map +1 -1
- package/dist/adapters/doom/voiceQuestionHandoff.mjs +2 -2
- package/dist/adapters/doom/voiceQuestionHandoff.mjs.map +1 -1
- package/dist/adapters/pi/activeToolRegistry.cjs +2 -0
- package/dist/adapters/pi/activeToolRegistry.cjs.map +1 -0
- package/dist/adapters/pi/activeToolRegistry.mjs +2 -0
- package/dist/adapters/pi/activeToolRegistry.mjs.map +1 -0
- package/dist/adapters/pi/askUserQuestionAdapter.cjs +2 -2
- package/dist/adapters/pi/askUserQuestionAdapter.cjs.map +1 -1
- package/dist/adapters/pi/askUserQuestionAdapter.mjs +2 -2
- package/dist/adapters/pi/askUserQuestionAdapter.mjs.map +1 -1
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.d.cts +8 -0
- package/dist/adapters/pi/extension.d.cts.map +1 -0
- package/dist/adapters/pi/extension.d.mts +8 -0
- package/dist/adapters/pi/extension.d.mts.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/adapters/pi/reconcileAdapter.cjs +1 -1
- package/dist/adapters/pi/reconcileAdapter.cjs.map +1 -1
- package/dist/adapters/pi/reconcileAdapter.mjs +1 -1
- package/dist/adapters/pi/reconcileAdapter.mjs.map +1 -1
- package/dist/adapters/pi/rpcQuestionnaireAdapter.cjs +1 -1
- package/dist/adapters/pi/rpcQuestionnaireAdapter.cjs.map +1 -1
- package/dist/adapters/pi/rpcQuestionnaireAdapter.mjs +1 -1
- package/dist/adapters/pi/rpcQuestionnaireAdapter.mjs.map +1 -1
- package/dist/extensions/pi.cjs +1 -2
- package/dist/extensions/pi.d.cts +2 -6
- package/dist/extensions/pi.d.mts +2 -6
- package/dist/extensions/pi.mjs +1 -2
- package/dist/index.cjs +0 -1
- package/dist/index.d.cts +3 -4
- package/dist/index.d.mts +3 -4
- package/dist/index.mjs +1 -1
- package/dist/services/answerEnvelope.cjs +2 -0
- package/dist/services/answerEnvelope.cjs.map +1 -0
- package/dist/services/answerEnvelope.mjs +2 -0
- package/dist/services/answerEnvelope.mjs.map +1 -0
- package/dist/services/askUserToolGate.cjs +2 -0
- package/dist/services/askUserToolGate.cjs.map +1 -0
- package/dist/services/askUserToolGate.mjs +2 -0
- package/dist/services/askUserToolGate.mjs.map +1 -0
- package/dist/services/autonomousVoiceMode.cjs +2 -0
- package/dist/services/autonomousVoiceMode.cjs.map +1 -0
- package/dist/services/autonomousVoiceMode.mjs +2 -0
- package/dist/services/autonomousVoiceMode.mjs.map +1 -0
- package/dist/services/questionnaireCoordinator.cjs +1 -1
- package/dist/services/questionnaireCoordinator.cjs.map +1 -1
- package/dist/services/questionnaireCoordinator.mjs +1 -1
- package/dist/services/questionnaireCoordinator.mjs.map +1 -1
- package/dist/types/askUserWire.cjs +2 -0
- package/dist/types/askUserWire.cjs.map +1 -0
- package/dist/types/askUserWire.mjs +2 -0
- package/dist/types/askUserWire.mjs.map +1 -0
- package/dist/types/questionnaire.d.cts +3 -1
- package/dist/types/questionnaire.d.cts.map +1 -1
- package/dist/types/questionnaire.d.mts +3 -1
- package/dist/types/questionnaire.d.mts.map +1 -1
- package/package.json +50 -24
- package/src/exports/webClient.ts +1 -0
- package/src/types/askUserWire.ts +45 -0
- package/src/types/questionnaire.ts +44 -0
- package/src/web/AskUserQuestionToolMessage.tsx +87 -0
- package/src/web/QuestionnairePrompt.tsx +370 -0
- package/src/web/askUserText.ts +65 -0
- package/src/web/index.ts +30 -0
- package/src/web/questionnaireDraft.ts +149 -0
- package/src/web/tsconfig.json +23 -0
- package/dist/adapters/doom/voiceQuestionHandoff.d.cts +0 -32
- package/dist/adapters/doom/voiceQuestionHandoff.d.cts.map +0 -1
- package/dist/adapters/doom/voiceQuestionHandoff.d.mts +0 -32
- package/dist/adapters/doom/voiceQuestionHandoff.d.mts.map +0 -1
- package/dist/events.cjs +0 -1
- package/dist/events.d.cts +0 -2
- package/dist/events.d.mts +0 -2
- package/dist/events.mjs +0 -1
- package/dist/extensions/pi.cjs.map +0 -1
- package/dist/extensions/pi.d.cts.map +0 -1
- package/dist/extensions/pi.d.mts.map +0 -1
- package/dist/extensions/pi.mjs.map +0 -1
- package/dist/schemas/events.cjs +0 -1
- package/dist/schemas/events.d.cts +0 -22
- package/dist/schemas/events.d.cts.map +0 -1
- package/dist/schemas/events.d.mts +0 -22
- package/dist/schemas/events.d.mts.map +0 -1
- package/dist/schemas/events.mjs +0 -1
- package/dist/voiceQuestionHandoff.cjs +0 -1
- package/dist/voiceQuestionHandoff.d.cts +0 -4
- package/dist/voiceQuestionHandoff.d.mts +0 -4
- package/dist/voiceQuestionHandoff.mjs +0 -1
package/README.md
CHANGED
|
@@ -1,59 +1,114 @@
|
|
|
1
1
|
# @agimon-ai/doompi-user-feedback
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ask structured questions in interactive Pi sessions, including sessions using autonomous Voice.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
terminal session it opens a blocking questionnaire and returns the selected answer to the
|
|
7
|
-
agent. The question does not get buried in prose and the turn does not continue on a
|
|
8
|
-
guess.
|
|
5
|
+
Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
plain text, hands them to narration, terminates the turn, and accepts the next spoken reply
|
|
12
|
-
as an ordinary user message.
|
|
7
|
+
The `ask_user_question` tool presents concrete options and waits for the user to choose. It does not let the model guess.
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
packages. It keeps the established `rpiv:ask-user:prompt` and `rpiv:ask-user:blocked`
|
|
16
|
-
channel names so existing Doompi listeners can migrate without being rewritten.
|
|
9
|
+
> **Alpha:** tool and Voice-handoff contracts may change between releases.
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
`ask-user` layer loads it where structured feedback is wanted.
|
|
11
|
+
## Requirements
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install @agimon-ai/doompi-user-feedback
|
|
25
|
-
```
|
|
13
|
+
- Node.js 22.19.0 or newer
|
|
14
|
+
- Pi 0.84.4 and Pi TUI 0.84.4
|
|
26
15
|
|
|
27
|
-
##
|
|
16
|
+
## Install
|
|
28
17
|
|
|
29
|
-
|
|
18
|
+
Add the package to a DoomPi layer. DoomPi preserves authored layer and package occurrence order,
|
|
19
|
+
while activating this package once per canonical resolved path:
|
|
30
20
|
|
|
31
21
|
```yaml
|
|
32
22
|
layers:
|
|
33
|
-
|
|
23
|
+
feedback:
|
|
34
24
|
packages: ['@agimon-ai/doompi-user-feedback']
|
|
25
|
+
```
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
For standalone Pi:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install npm:@agimon-ai/doompi-user-feedback
|
|
38
31
|
```
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
Pi loads the package through `package.json.pi.extensions`. Each factory invocation owns one package
|
|
34
|
+
instance. Session replacement resets its state, and shutdown removes its registrations.
|
|
35
|
+
|
|
36
|
+
## Ask a question
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"questions": [
|
|
41
|
+
{
|
|
42
|
+
"header": "Storage",
|
|
43
|
+
"question": "Where should session results be stored?",
|
|
44
|
+
"options": [
|
|
45
|
+
{
|
|
46
|
+
"label": "Project files",
|
|
47
|
+
"description": "Easy to inspect and commit, but visible in the repository."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"label": "User state",
|
|
51
|
+
"description": "Keeps generated state outside the repository."
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"multiSelect": false
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A call accepts 1 to 4 questions. Each question requires 2 to 4 options. Headers are at most 16
|
|
61
|
+
characters; option labels are at most 60 characters. Use `multiSelect: true` when several choices
|
|
62
|
+
are valid. An option can include a Markdown `preview` for meaningful visual comparison.
|
|
63
|
+
|
|
64
|
+
The tool result contains readable answer text plus structured `details.answers`; cancellation sets
|
|
65
|
+
`cancelled: true`. Pressing Escape declines the questionnaire. Do not add an `Other` option because
|
|
66
|
+
the UI supplies a custom-text row.
|
|
42
67
|
|
|
43
|
-
##
|
|
68
|
+
## UI, RPC, and headless behavior
|
|
44
69
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
session ends so subscriptions do not outlive their owner.
|
|
70
|
+
The tool requires an interactive UI. It uses the TUI questionnaire when available and can use Pi's
|
|
71
|
+
dialog or RPC UI bridge. A headless context without either UI returns a structured `no_ui`
|
|
72
|
+
cancellation rather than waiting forever.
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
Questionnaires are coordinated one at a time. Cancellation, session replacement, or shutdown
|
|
75
|
+
aborts the active prompt and settles queued work.
|
|
76
|
+
|
|
77
|
+
## Autonomous Voice
|
|
78
|
+
|
|
79
|
+
While autonomous Voice mode is active, `ask_user_question` leaves Pi's active tool set. Voice already
|
|
80
|
+
speaks its own question through `narrate` and then waits for the spoken reply, so keeping a
|
|
81
|
+
questionnaire tool alongside it asks the same thing twice. The agent asks by narrating, and the
|
|
82
|
+
answer arrives as an ordinary user message.
|
|
83
|
+
|
|
84
|
+
The gate tracks its own removal. A tool switched off by the user stays off, no other package's tools
|
|
85
|
+
are touched, and the removal is restored when Voice deactivates, when the Voice services unload, or
|
|
86
|
+
when this runtime shuts down.
|
|
87
|
+
|
|
88
|
+
A call already in flight when Voice activates still hands off: the extension formats the questions as
|
|
89
|
+
plain text, requests narration, and returns `terminate: true`, ending the turn while it waits.
|
|
90
|
+
|
|
91
|
+
Both paths use the shared `doom/minor-mode-catalog` and `doom/narration` Cordis services. If a
|
|
92
|
+
provider unloads, User Feedback restores the tool and falls back to its interactive questionnaire
|
|
93
|
+
without retaining the old provider.
|
|
52
94
|
|
|
53
95
|
## Public API
|
|
54
96
|
|
|
55
|
-
|
|
56
|
-
|
|
97
|
+
Question and result types are available from the package root. Cross-extension ask-user events and
|
|
98
|
+
Voice services come from `@agimon-ai/doompi-extension-contracts`; consumers should use those Cordis
|
|
99
|
+
contracts. Pi loads the default export of `/extensions/pi` through package metadata, so installation
|
|
100
|
+
does not require a manual registration call.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm build
|
|
106
|
+
pnpm typecheck
|
|
107
|
+
pnpm test
|
|
108
|
+
pnpm lint
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Maintained by [Agimon](https://agimon.ai/about).
|
|
57
112
|
|
|
58
113
|
## License
|
|
59
114
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const e=require("../../services/
|
|
1
|
+
const e=require("../../services/autonomousVoiceMode.cjs"),t=require("../../services/voiceQuestionNarration.cjs");let n=require("@agimon-ai/doompi-extension-contracts/narration");const r=`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 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
|
|
4
|
+
`)}var a=class{modes;narration;buildNarration;constructor(e,n,r=t.buildVoiceQuestionNarration){this.modes=e,this.narration=n,this.buildNarration=r}async handoff(t){if(!this.modes.list().some(t=>e.isAutonomousVoiceRecord(t)))return;let r=(0,n.createNarrationRequest)(this.buildNarration(t));if(r)return await this.narration.request(r),{answers:[],cancelled:!1,delivery:`voice`,awaitingResponse:!0,voicePrompt:i(t)}}};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${r}`:r}],details:e,terminate:!0}}exports.VOICE_WAIT_MESSAGE=r,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":["
|
|
1
|
+
{"version":3,"file":"voiceQuestionHandoff.cjs","names":["buildVoiceQuestionNarration","isAutonomousVoiceRecord","createNarrationRequest"],"sources":["../../../src/adapters/doom/voiceQuestionHandoff.ts"],"sourcesContent":["import type { MinorModeCatalogService } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { createNarrationRequest, type DoomNarrationService } from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { QuestionParams } from '../../schemas/questionnaire.js';\nimport { isAutonomousVoiceRecord } from '../../services/autonomousVoiceMode.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\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((record) => isAutonomousVoiceRecord(record))) 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":"kLAOA,MAAa,EACX,iJAEF,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,EAAsEA,EAAAA,4BACtE,CAHiB,KAAA,MAAA,EACA,KAAA,UAAA,EACA,KAAA,eAAA,CAChB,CAEH,MAAM,QAAQ,EAAkE,CAC9E,GAAI,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC,KAAM,GAAWC,EAAAA,wBAAwB,CAAM,CAAC,EAAG,OAC1E,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
|
-
import{
|
|
1
|
+
import{isAutonomousVoiceRecord as e}from"../../services/autonomousVoiceMode.mjs";import{buildVoiceQuestionNarration as t}from"../../services/voiceQuestionNarration.mjs";import{createNarrationRequest as n}from"@agimon-ai/doompi-extension-contracts/narration";const r=`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 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
|
|
4
|
+
`)}var a=class{modes;narration;buildNarration;constructor(e,n,r=t){this.modes=e,this.narration=n,this.buildNarration=r}async handoff(t){if(!this.modes.list().some(t=>e(t)))return;let r=n(this.buildNarration(t));if(r)return await this.narration.request(r),{answers:[],cancelled:!1,delivery:`voice`,awaitingResponse:!0,voicePrompt:i(t)}}};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${r}`:r}],details:e,terminate:!0}}export{r as VOICE_WAIT_MESSAGE,a as VoiceQuestionHandoff,s as buildVoiceToolResult,o as createVoiceQuestionHandoff,i 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
|
|
1
|
+
{"version":3,"file":"voiceQuestionHandoff.mjs","names":[],"sources":["../../../src/adapters/doom/voiceQuestionHandoff.ts"],"sourcesContent":["import type { MinorModeCatalogService } from '@agimon-ai/doompi-extension-contracts/mode';\nimport { createNarrationRequest, type DoomNarrationService } from '@agimon-ai/doompi-extension-contracts/narration';\nimport type { QuestionParams } from '../../schemas/questionnaire.js';\nimport { isAutonomousVoiceRecord } from '../../services/autonomousVoiceMode.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\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((record) => isAutonomousVoiceRecord(record))) 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":"kQAOA,MAAa,EACX,iJAEF,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,KAAM,GAAW,EAAwB,CAAM,CAAC,EAAG,OAC1E,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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activeToolRegistry.cjs","names":[],"sources":["../../../src/adapters/pi/activeToolRegistry.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { ActiveToolRegistry } from '../../types/toolActivation.js';\n\n/**\n * Adapts Pi's extension API to the gate's port.\n *\n * Returns undefined for a host that does not expose the runtime tool list. Those hosts\n * keep every registered tool active, so there is nothing for the gate to drive and no\n * reason to fail the install over it.\n */\nexport function readActiveToolRegistry(pi: ExtensionAPI): ActiveToolRegistry | undefined {\n const candidate = pi as Partial<ActiveToolRegistry>;\n if (typeof candidate.getActiveTools !== 'function' || typeof candidate.setActiveTools !== 'function') {\n return undefined;\n }\n return {\n getActiveTools: () => pi.getActiveTools(),\n setActiveTools: (toolNames) => pi.setActiveTools(toolNames),\n };\n}\n"],"mappings":"AAUA,SAAgB,EAAuB,EAAkD,CACvF,IAAM,EAAY,EACd,UAAO,EAAU,gBAAmB,YAAc,OAAO,EAAU,gBAAmB,WAG1F,MAAO,CACL,mBAAsB,EAAG,eAAe,EACxC,eAAiB,GAAc,EAAG,eAAe,CAAS,CAC5D,CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activeToolRegistry.mjs","names":[],"sources":["../../../src/adapters/pi/activeToolRegistry.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { ActiveToolRegistry } from '../../types/toolActivation.js';\n\n/**\n * Adapts Pi's extension API to the gate's port.\n *\n * Returns undefined for a host that does not expose the runtime tool list. Those hosts\n * keep every registered tool active, so there is nothing for the gate to drive and no\n * reason to fail the install over it.\n */\nexport function readActiveToolRegistry(pi: ExtensionAPI): ActiveToolRegistry | undefined {\n const candidate = pi as Partial<ActiveToolRegistry>;\n if (typeof candidate.getActiveTools !== 'function' || typeof candidate.setActiveTools !== 'function') {\n return undefined;\n }\n return {\n getActiveTools: () => pi.getActiveTools(),\n setActiveTools: (toolNames) => pi.setActiveTools(toolNames),\n };\n}\n"],"mappings":"AAUA,SAAgB,EAAuB,EAAkD,CACvF,IAAM,EAAY,EACd,UAAO,EAAU,gBAAmB,YAAc,OAAO,EAAU,gBAAmB,WAG1F,MAAO,CACL,mBAAsB,EAAG,eAAe,EACxC,eAAiB,GAAc,EAAG,eAAe,CAAS,CAC5D,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}))}))};(
|
|
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:
|
|
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{
|
|
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:
|
|
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/askUserToolGate.cjs"),t=require("../../services/autonomousVoiceMode.cjs"),n=require("../../services/questionnaireCoordinator.cjs"),r=require("../doom/voiceQuestionHandoff.cjs"),i=require("./activeToolRegistry.cjs"),a=require("./askUserQuestionAdapter.cjs"),o=require("./reconcileAdapter.cjs");let s=require("@agimon-ai/doompi-extension-contracts/cordis-host"),c=require("@agimon-ai/doompi-extension-contracts/mode"),l=require("@agimon-ai/doompi-extension-contracts/narration");const u=`@agimon-ai/doompi-user-feedback`;function d(){return{answers:[],cancelled:!0}}function f(s,f){let p=!0,m=0,h,g=!1,_=new n.QuestionnaireCoordinator,v,y=i.readActiveToolRegistry(f),b=y?new e.AskUserToolGate(y,a.ASK_USER_QUESTION_TOOL_NAME):void 0,x=()=>void 0,S=Promise.resolve(),C=new Set,w=e=>(C.add(e),e.then(()=>C.delete(e),()=>C.delete(e)),e),T=(e,t)=>p&&e===m&&t===h,E=(e,t)=>p&&g&&t?.aborted!==!0&&e.sessionManager.getSessionId()===h,D=async()=>{if(!p)return;b?.release();let e=_;e.shutdown(),p=!1,m+=1,g=!1,h=void 0,await Promise.allSettled(C),await e.waitForIdle()};s.effect(()=>D,`${u}/runtime`),s.inject([c.DOOM_MINOR_MODE_CATALOG_SERVICE,l.DOOM_NARRATION_SERVICE],e=>{let t=r.createVoiceQuestionHandoff((0,c.requireMinorModeCatalog)(e),(0,l.requireDoomNarrationService)(e));return v=t,()=>{v===t&&(v=void 0)}}),s.inject([c.DOOM_MINOR_MODE_CATALOG_SERVICE],e=>{if(!b)return;let n=(0,c.requireMinorModeCatalog)(e),r=()=>{p&&g&&b.sync(t.isAutonomousVoiceActive(n.list()))};x=r;let i=n.subscribe(r);return r(),()=>{i(),x===r&&(x=()=>void 0),b.release()}}),a.registerAskUserQuestionTool(f,s,{enqueue:(e,t)=>{let n=_,r=m,i=h;return!g||!T(r,i)||t?.aborted?Promise.resolve(d()):n.enqueue(async t=>{if(!g||!T(r,i)||t.signal.aborted)return d();let n=await e({...t,reportProgress:e=>{T(r,i)&&!t.signal.aborted&&t.reportProgress(e)}});return g&&T(r,i)&&!t.signal.aborted?n:d()},t)},isActive:E,runTui:async(e,t,n,r,i)=>{if(!E(e,r))return;let{runTuiQuestionnaire:a}=await Promise.resolve().then(()=>require("../../tui/runQuestionnaire.cjs"));if(!E(e,r))return;let o=await a(e,t,n,r,i);return E(e,r)?o:void 0},tryVoice:e=>p&&g?v?.handoff(e):void 0}),o.registerAskUserQuestionReconciler(f,E),f.on(`session_start`,(e,t)=>{if(!p)return;let r=_;r.shutdown();let i=t.sessionManager.getSessionId(),a=++m;_=new n.QuestionnaireCoordinator,h=i,g=!1;let o=async()=>{await r.waitForIdle(),T(a,i)&&(g=!0,x())},s=S.then(o,o);return S=s.catch(()=>void 0),w(s)})}async function p(e){let t=await(0,s.connectDoomCordisHost)(e,u),n=t.root.plugin(m,{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 m(e,t){f(e,t.pi)}exports.default=p,exports.userFeedbackExtension=p,exports.installUserFeedbackRuntime=f;
|
|
2
|
+
//# sourceMappingURL=extension.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.cjs","names":["QuestionnaireCoordinator","readActiveToolRegistry","AskUserToolGate","ASK_USER_QUESTION_TOOL_NAME","DOOM_MINOR_MODE_CATALOG_SERVICE","DOOM_NARRATION_SERVICE","createVoiceQuestionHandoff","requireMinorModeCatalog","requireDoomNarrationService","isAutonomousVoiceActive","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 { AskUserToolGate } from '../../services/askUserToolGate.js';\nimport { isAutonomousVoiceActive } from '../../services/autonomousVoiceMode.js';\nimport { QuestionnaireCoordinator, type QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { createVoiceQuestionHandoff, type VoiceQuestionHandoff } from '../doom/voiceQuestionHandoff.js';\nimport { readActiveToolRegistry } from './activeToolRegistry.js';\nimport { ASK_USER_QUESTION_TOOL_NAME, 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 const registry = readActiveToolRegistry(pi);\n const toolGate = registry ? new AskUserToolGate(registry, ASK_USER_QUESTION_TOOL_NAME) : undefined;\n let syncToolGate = (): void => 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 // Released before the flags drop, so a runtime torn down while Voice is still active\n // cannot leave the tool hidden: Pi re-activates a refreshed tool only when its name is\n // new to the registry, and after a reload this one is not.\n toolGate?.release();\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 // Separate from the handoff binding above: the gate needs only the mode catalog, so it\n // still applies to a Voice build that publishes modes without providing narration.\n cordis.inject([DOOM_MINOR_MODE_CATALOG_SERVICE], (modeContext) => {\n if (!toolGate) return undefined;\n const modes = requireMinorModeCatalog(modeContext);\n const apply = (): void => {\n // Pi binds the active-tool accessors to a started session runtime and throws before\n // that, so nothing is applied until the session is ready.\n if (active && sessionReady) toolGate.sync(isAutonomousVoiceActive(modes.list()));\n };\n syncToolGate = apply;\n const unsubscribe = modes.subscribe(apply);\n apply();\n return () => {\n unsubscribe();\n if (syncToolGate === apply) syncToolGate = (): void => undefined;\n toolGate.release();\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 // A session can start with Voice already active, for example across a Voice reload.\n syncToolGate();\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":"6fAcA,MAAM,EAAiB,kCAGvB,SAAS,GAAuC,CAC9C,MAAO,CAAE,QAAS,CAAC,EAAG,UAAW,EAAK,CACxC,CAGA,SAAgB,EAA2B,EAAiB,EAAwB,CAClF,IAAI,EAAS,GACT,EAAoB,EACpB,EACA,EAAe,GACf,EAAc,IAAIA,EAAAA,yBAClB,EACE,EAAWC,EAAAA,uBAAuB,CAAE,EACpC,EAAW,EAAW,IAAIC,EAAAA,gBAAgB,EAAUC,EAAAA,2BAA2B,EAAI,IAAA,GACrF,MAA2B,IAAA,GAC3B,EAAgC,QAAQ,QAAQ,EAC9C,EAAoB,IAAI,IAExB,EAAqB,IACzB,EAAkB,IAAI,CAAS,EAC/B,EAAe,SACP,EAAkB,OAAO,CAAS,MAClC,EAAkB,OAAO,CAAS,CAC1C,EACO,GAEH,GAAkB,EAAoB,IAC1C,GAAU,IAAe,GAAqB,IAAsB,EAChE,GAAmB,EAA2B,IAClD,GAAU,GAAgB,GAAQ,UAAY,IAAQ,EAAQ,eAAe,aAAa,IAAM,EAE5F,EAAkB,SAA2B,CACjD,GAAI,CAAC,EAAQ,OAIb,GAAU,QAAQ,EAClB,IAAM,EAAmB,EACzB,EAAiB,SAAS,EAC1B,EAAS,GACT,GAAqB,EACrB,EAAe,GACf,EAAY,IAAA,GACZ,MAAM,QAAQ,WAAW,CAAiB,EAC1C,MAAM,EAAiB,YAAY,CACrC,EACA,EAAO,WAAa,EAAiB,GAAG,EAAe,SAAS,EAEhE,EAAO,OAAO,CAACC,EAAAA,gCAAiCC,EAAAA,sBAAsB,EAAI,GAAmB,CAC3F,IAAM,EAAUC,EAAAA,4BAAAA,EACdC,EAAAA,wBAAAA,CAAwB,CAAc,GAAA,EACtCC,EAAAA,4BAAAA,CAA4B,CAAc,CAC5C,EAEA,MADA,GAAe,MACF,CACP,IAAiB,IAAS,EAAe,IAAA,GAC/C,CACF,CAAC,EAID,EAAO,OAAO,CAACJ,EAAAA,+BAA+B,EAAI,GAAgB,CAChE,GAAI,CAAC,EAAU,OACf,IAAM,GAAA,EAAQG,EAAAA,wBAAAA,CAAwB,CAAW,EAC3C,MAAoB,CAGpB,GAAU,GAAc,EAAS,KAAKE,EAAAA,wBAAwB,EAAM,KAAK,CAAC,CAAC,CACjF,EACA,EAAe,EACf,IAAM,EAAc,EAAM,UAAU,CAAK,EAEzC,OADA,EAAM,MACO,CACX,EAAY,EACR,IAAiB,IAAO,MAA2B,IAAA,IACvD,EAAS,QAAQ,CACnB,CACF,CAAC,EAED,EAAA,4BAA4B,EAAI,EAAQ,CACtC,SAAU,EAA6B,IAAyB,CAC9D,IAAM,EAAmB,EACnB,EAAa,EACb,EAAoB,EAI1B,MAHI,CAAC,GAAgB,CAAC,EAAe,EAAY,CAAiB,GAAK,GAAQ,QACtE,QAAQ,QAAQ,EAAgB,CAAC,EAEnC,EAAiB,QAAQ,KAAO,IAAY,CACjD,GAAI,CAAC,GAAgB,CAAC,EAAe,EAAY,CAAiB,GAAK,EAAQ,OAAO,QACpF,OAAO,EAAgB,EAEzB,IAAM,EAAS,MAAM,EAAO,CAC1B,GAAG,EACH,eAAiB,GAAa,CACxB,EAAe,EAAY,CAAiB,GAAK,CAAC,EAAQ,OAAO,SACnE,EAAQ,eAAe,CAAQ,CAEnC,CACF,CAAC,EACD,OAAO,GAAgB,EAAe,EAAY,CAAiB,GAAK,CAAC,EAAQ,OAAO,QACpF,EACA,EAAgB,CACtB,EAAG,CAAM,CACX,EACA,SAAU,EACV,OAAQ,MAAO,EAAS,EAAQ,EAAa,EAAQ,IAAmB,CACtE,GAAI,CAAC,EAAgB,EAAS,CAAM,EAAG,OACvC,GAAM,CAAE,uBAAwB,MAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAM,gCAAA,CAAA,EACtC,GAAI,CAAC,EAAgB,EAAS,CAAM,EAAG,OACvC,IAAM,EAAS,MAAM,EAAoB,EAAS,EAAQ,EAAa,EAAQ,CAAc,EAC7F,OAAO,EAAgB,EAAS,CAAM,EAAI,EAAS,IAAA,EACrD,EACA,SAAW,GAAY,GAAU,EAAe,GAAc,QAAQ,CAAM,EAAI,IAAA,EAClF,CAAC,EACD,EAAA,kCAAkC,EAAI,CAAe,EAErD,EAAG,GAAG,iBAAsB,EAAQ,IAAY,CAC9C,GAAI,CAAC,EAAQ,OACb,IAAM,EAAsB,EAC5B,EAAoB,SAAS,EAC7B,IAAM,EAAkB,EAAQ,eAAe,aAAa,EACtD,EAAa,EAAE,EAErB,EAAc,IADcT,EAAAA,yBAE5B,EAAY,EACZ,EAAe,GAEf,IAAM,EAAoB,SAA2B,CACnD,MAAM,EAAoB,YAAY,EACjC,EAAe,EAAY,CAAe,IAC/C,EAAe,GAEf,EAAa,EACf,EAEM,EAAY,EAAe,KAAK,EAAmB,CAAiB,EAE1E,MADA,GAAiB,EAAU,UAAY,IAAA,EAAS,EACzC,EAAe,CAAS,CACjC,CAAC,CACH,CAGA,eAAsB,EAAsB,EAAiC,CAC3E,IAAM,EAAa,MAAA,EAAMU,EAAAA,sBAAAA,CAAsB,EAAI,CAAc,EAC3D,EAAQ,EAAW,KAAK,OAAO,EAAoB,CAAE,IAAG,CAAC,EAC/D,GAAI,CACF,MAAM,CACR,OAAS,EAAO,CACd,GAAI,CACF,MAAM,EAAM,QAAQ,CACtB,QAAU,CACR,MAAM,EAAW,QAAQ,CAC3B,CACA,MAAM,CACR,CACA,IAAI,EACJ,EAAG,GACD,uBAEG,KAAc,SAAY,CACzB,GAAI,CACF,MAAM,EAAM,QAAQ,CACtB,QAAU,CACR,MAAM,EAAW,QAAQ,CAC3B,CACF,EAAA,CAAG,CACP,CACF,CAMA,SAAS,EAAmB,EAAiB,EAAwC,CACnF,EAA2B,EAAQ,EAAO,EAAE,CAC9C"}
|
|
@@ -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+JsB,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+JsB,sBAAsB,IAAI,eAAe"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AskUserToolGate as e}from"../../services/askUserToolGate.mjs";import{isAutonomousVoiceActive as t}from"../../services/autonomousVoiceMode.mjs";import{QuestionnaireCoordinator as n}from"../../services/questionnaireCoordinator.mjs";import{createVoiceQuestionHandoff as r}from"../doom/voiceQuestionHandoff.mjs";import{readActiveToolRegistry as i}from"./activeToolRegistry.mjs";import{ASK_USER_QUESTION_TOOL_NAME as a,registerAskUserQuestionTool as o}from"./askUserQuestionAdapter.mjs";import{registerAskUserQuestionReconciler as s}from"./reconcileAdapter.mjs";import{connectDoomCordisHost as c}from"@agimon-ai/doompi-extension-contracts/cordis-host";import{DOOM_MINOR_MODE_CATALOG_SERVICE as l,requireMinorModeCatalog as u}from"@agimon-ai/doompi-extension-contracts/mode";import{DOOM_NARRATION_SERVICE as d,requireDoomNarrationService as f}from"@agimon-ai/doompi-extension-contracts/narration";const p=`@agimon-ai/doompi-user-feedback`;function m(){return{answers:[],cancelled:!0}}function h(c,h){let g=!0,_=0,v,y=!1,b=new n,x,S=i(h),C=S?new e(S,a):void 0,w=()=>void 0,T=Promise.resolve(),E=new Set,D=e=>(E.add(e),e.then(()=>E.delete(e),()=>E.delete(e)),e),O=(e,t)=>g&&e===_&&t===v,k=(e,t)=>g&&y&&t?.aborted!==!0&&e.sessionManager.getSessionId()===v,A=async()=>{if(!g)return;C?.release();let e=b;e.shutdown(),g=!1,_+=1,y=!1,v=void 0,await Promise.allSettled(E),await e.waitForIdle()};c.effect(()=>A,`${p}/runtime`),c.inject([l,d],e=>{let t=r(u(e),f(e));return x=t,()=>{x===t&&(x=void 0)}}),c.inject([l],e=>{if(!C)return;let n=u(e),r=()=>{g&&y&&C.sync(t(n.list()))};w=r;let i=n.subscribe(r);return r(),()=>{i(),w===r&&(w=()=>void 0),C.release()}}),o(h,c,{enqueue:(e,t)=>{let n=b,r=_,i=v;return!y||!O(r,i)||t?.aborted?Promise.resolve(m()):n.enqueue(async t=>{if(!y||!O(r,i)||t.signal.aborted)return m();let n=await e({...t,reportProgress:e=>{O(r,i)&&!t.signal.aborted&&t.reportProgress(e)}});return y&&O(r,i)&&!t.signal.aborted?n:m()},t)},isActive:k,runTui:async(e,t,n,r,i)=>{if(!k(e,r))return;let{runTuiQuestionnaire:a}=await import(`../../tui/runQuestionnaire.mjs`);if(!k(e,r))return;let o=await a(e,t,n,r,i);return k(e,r)?o:void 0},tryVoice:e=>g&&y?x?.handoff(e):void 0}),s(h,k),h.on(`session_start`,(e,t)=>{if(!g)return;let r=b;r.shutdown();let i=t.sessionManager.getSessionId(),a=++_;b=new n,v=i,y=!1;let o=async()=>{await r.waitForIdle(),O(a,i)&&(y=!0,w())},s=T.then(o,o);return T=s.catch(()=>void 0),D(s)})}async function g(e){let t=await c(e,p),n=t.root.plugin(_,{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 _(e,t){h(e,t.pi)}export{g as default,g as userFeedbackExtension,h 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 { AskUserToolGate } from '../../services/askUserToolGate.js';\nimport { isAutonomousVoiceActive } from '../../services/autonomousVoiceMode.js';\nimport { QuestionnaireCoordinator, type QuestionnaireRunner } from '../../services/questionnaireCoordinator.js';\nimport type { QuestionnaireResult } from '../../types/questionnaire.js';\nimport { createVoiceQuestionHandoff, type VoiceQuestionHandoff } from '../doom/voiceQuestionHandoff.js';\nimport { readActiveToolRegistry } from './activeToolRegistry.js';\nimport { ASK_USER_QUESTION_TOOL_NAME, 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 const registry = readActiveToolRegistry(pi);\n const toolGate = registry ? new AskUserToolGate(registry, ASK_USER_QUESTION_TOOL_NAME) : undefined;\n let syncToolGate = (): void => 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 // Released before the flags drop, so a runtime torn down while Voice is still active\n // cannot leave the tool hidden: Pi re-activates a refreshed tool only when its name is\n // new to the registry, and after a reload this one is not.\n toolGate?.release();\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 // Separate from the handoff binding above: the gate needs only the mode catalog, so it\n // still applies to a Voice build that publishes modes without providing narration.\n cordis.inject([DOOM_MINOR_MODE_CATALOG_SERVICE], (modeContext) => {\n if (!toolGate) return undefined;\n const modes = requireMinorModeCatalog(modeContext);\n const apply = (): void => {\n // Pi binds the active-tool accessors to a started session runtime and throws before\n // that, so nothing is applied until the session is ready.\n if (active && sessionReady) toolGate.sync(isAutonomousVoiceActive(modes.list()));\n };\n syncToolGate = apply;\n const unsubscribe = modes.subscribe(apply);\n apply();\n return () => {\n unsubscribe();\n if (syncToolGate === apply) syncToolGate = (): void => undefined;\n toolGate.release();\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 // A session can start with Voice already active, for example across a Voice reload.\n syncToolGate();\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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"}
|