@agimon-ai/doompi-user-feedback 0.0.1-alpha.34 → 0.0.1-alpha.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-user-feedback",
3
- "version": "0.0.1-alpha.34",
3
+ "version": "0.0.1-alpha.35",
4
4
  "description": "Structured user questions with interactive and autonomous Voice handoff for Pi agents.",
5
5
  "keywords": [
6
6
  "ai",
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "web",
30
+ "src/web",
31
+ "src/exports/webClient.ts",
31
32
  "src/types/askUserWire.ts",
32
33
  "src/types/questionnaire.ts",
33
34
  "README.md",
@@ -57,10 +58,10 @@
57
58
  "dependencies": {
58
59
  "@deepseek-ai/cordis": "4.0.1",
59
60
  "typebox": "1.3.19",
60
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.44",
61
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.5",
62
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.8",
63
- "@agimon-ai/doompi-ui": "0.0.1-alpha.45"
61
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.45",
62
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.46",
63
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.6",
64
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.9"
64
65
  },
65
66
  "devDependencies": {
66
67
  "@earendil-works/pi-coding-agent": "0.84.4",
@@ -93,7 +94,7 @@
93
94
  "doompiWeb": {
94
95
  "pluginId": "ask-user",
95
96
  "channels": [],
96
- "client": "./web/index.ts"
97
+ "client": "./src/exports/webClient.ts"
97
98
  },
98
99
  "pi": {
99
100
  "extensions": [
@@ -103,7 +104,7 @@
103
104
  "scripts": {
104
105
  "build": "tsdown",
105
106
  "test": "vitest --run",
106
- "typecheck": "tsc --noEmit && tsc --noEmit -p web/tsconfig.json",
107
+ "typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.json",
107
108
  "lint": "oxlint . && oxfmt . --check",
108
109
  "fixcode": "oxlint . --fix && oxfmt ."
109
110
  }
@@ -0,0 +1 @@
1
+ export { webPlugin } from '../web/index.ts';
@@ -10,8 +10,8 @@ import {
10
10
  Textarea,
11
11
  } from '@agimon-ai/doompi-web-components';
12
12
  import type { ToolPromptRenderProps } from '@agimon-ai/doompi-web-contracts';
13
- import { useEffect, useMemo, useRef, useState } from 'react';
14
- import { encodeAnswerEnvelope } from '../src/types/askUserWire.ts';
13
+ import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react';
14
+ import { encodeAnswerEnvelope } from '../types/askUserWire.ts';
15
15
  import {
16
16
  chooseOption,
17
17
  CUSTOM_LABEL,
@@ -70,6 +70,23 @@ function StepBar({
70
70
  );
71
71
  }
72
72
 
73
+ /**
74
+ * The frame's shortcuts stop at a text field: while the caret is in one the
75
+ * arrows and Enter belong to the field, so they are kept from bubbling. Enter
76
+ * commits the field the way the terminal editor does, Shift+Enter writes a
77
+ * newline, and Escape and Cmd/Ctrl+Enter still reach the frame.
78
+ */
79
+ function fieldKeys(commit: () => void) {
80
+ return (event: KeyboardEvent<HTMLTextAreaElement>): void => {
81
+ if (event.key === 'Escape') return;
82
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) return;
83
+ event.stopPropagation();
84
+ if (event.key !== 'Enter' || event.shiftKey) return;
85
+ event.preventDefault();
86
+ commit();
87
+ };
88
+ }
89
+
73
90
  /** One question's options, its typed-answer row, and the preview of whatever is focused. */
74
91
  function QuestionBody({
75
92
  question,
@@ -78,6 +95,7 @@ function QuestionBody({
78
95
  cursor,
79
96
  onCursor,
80
97
  onDraft,
98
+ onCommit,
81
99
  }: {
82
100
  question: PromptQuestion;
83
101
  index: number;
@@ -86,6 +104,8 @@ function QuestionBody({
86
104
  onCursor: (cursor: number) => void;
87
105
  /** `settles` is true only for an edit that finishes the question, which is what may move on. */
88
106
  onDraft: (draft: QuestionnaireDraft, settles: boolean) => void;
107
+ /** Enter in a text field: hands the keyboard back to the frame, settling the question or not. */
108
+ onCommit: (settles: boolean) => void;
89
109
  }) {
90
110
  const entry = draftEntry(draft, index);
91
111
  const typing = entry.custom !== null;
@@ -152,6 +172,7 @@ function QuestionBody({
152
172
  value={entry.custom ?? ''}
153
173
  placeholder="type your answer…"
154
174
  onChange={(event) => onDraft(setCustom(draft, index, event.target.value), false)}
175
+ onKeyDown={fieldKeys(() => onCommit(true))}
155
176
  className="text-[12px]"
156
177
  />
157
178
  </div>
@@ -174,6 +195,7 @@ function QuestionBody({
174
195
  value={entry.notes}
175
196
  placeholder="anything the options do not cover"
176
197
  onChange={(event) => onDraft(setNotes(draft, index, event.target.value), false)}
198
+ onKeyDown={fieldKeys(() => onCommit(false))}
177
199
  className="text-[11px]"
178
200
  />
179
201
  </div>
@@ -199,6 +221,9 @@ export function QuestionnairePrompt({ args, dialog, answer, cancel }: ToolPrompt
199
221
 
200
222
  // A new request is a new questionnaire; nothing from the last one carries over.
201
223
  useEffect(() => {
224
+ // Remounting on dialog.id would be the alternative, but the prompt host owns the
225
+ // element, so the reset lives here.
226
+ // oxlint-disable-next-line react/set-state-in-effect
202
227
  setDraft(emptyDraft(questions.length));
203
228
  setCurrent(0);
204
229
  setCursor(0);
@@ -262,6 +287,11 @@ export function QuestionnairePrompt({ args, dialog, answer, cancel }: ToolPrompt
262
287
  cancel();
263
288
  return;
264
289
  }
290
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
291
+ event.preventDefault();
292
+ submit();
293
+ return;
294
+ }
265
295
  // While typing an answer the arrows belong to the textarea.
266
296
  if (typing) return;
267
297
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
@@ -279,7 +309,7 @@ export function QuestionnairePrompt({ args, dialog, answer, cancel }: ToolPrompt
279
309
  if (event.key === 'Enter') {
280
310
  event.preventDefault();
281
311
  const option = question.options[cursor];
282
- if (event.metaKey || event.ctrlKey || option === undefined) {
312
+ if (option === undefined) {
283
313
  submit();
284
314
  return;
285
315
  }
@@ -305,6 +335,10 @@ export function QuestionnairePrompt({ args, dialog, answer, cancel }: ToolPrompt
305
335
  cursor={cursor}
306
336
  onCursor={setCursor}
307
337
  onDraft={(next, settles) => move(next, current, settles)}
338
+ onCommit={(settles) => {
339
+ frame.current?.focus();
340
+ move(draft, current, settles);
341
+ }}
308
342
  />
309
343
  </div>
310
344
 
@@ -10,7 +10,7 @@
10
10
  * A step the reader goes back to still has what they chose, and the agent is
11
11
  * asked once rather than once per question.
12
12
  */
13
- import type { QuestionAnswer } from '../src/types/questionnaire.ts';
13
+ import type { QuestionAnswer } from '../types/questionnaire.ts';
14
14
 
15
15
  /** The row every question gets, so a reader always has a way past a bad set of options. */
16
16
  export const CUSTOM_LABEL = 'Type something.';
@@ -19,5 +19,5 @@
19
19
  "forceConsistentCasingInFileNames": true,
20
20
  "types": ["node"]
21
21
  },
22
- "include": [".", "../src/types"]
22
+ "include": [".", "../types", "../exports/webClient.ts"]
23
23
  }
File without changes
File without changes
File without changes