@agimon-ai/doompi-user-feedback 0.0.1-alpha.26 → 0.0.1-alpha.28

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 CHANGED
@@ -13,7 +13,7 @@ guess.
13
13
  ## Requirements
14
14
 
15
15
  - Node.js 22.19.0 or newer
16
- - Pi 0.84.2 and Pi TUI 0.84.2
16
+ - Pi 0.84.3 and Pi TUI 0.84.3
17
17
 
18
18
  ## Install
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-user-feedback",
3
- "version": "0.0.1-alpha.26",
3
+ "version": "0.0.1-alpha.28",
4
4
  "description": "Structured user questions with interactive and autonomous Voice handoff for Pi agents.",
5
5
  "keywords": [
6
6
  "ai",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
+ "web",
30
31
  "README.md",
31
32
  "LICENSE",
32
33
  "package.json"
@@ -54,21 +55,27 @@
54
55
  "dependencies": {
55
56
  "@deepseek-ai/cordis": "4.0.1",
56
57
  "typebox": "1.3.16",
57
- "@agimon-ai/doompi-ui": "0.0.1-alpha.37",
58
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.37"
58
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.2",
59
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.39",
60
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.39",
61
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.0"
59
62
  },
60
63
  "devDependencies": {
61
- "@earendil-works/pi-coding-agent": "0.84.2",
62
- "@earendil-works/pi-tui": "0.84.2",
64
+ "@earendil-works/pi-coding-agent": "0.84.3",
65
+ "@earendil-works/pi-tui": "0.84.3",
66
+ "@tanstack/react-store": "0.11.1",
67
+ "@tanstack/store": "0.11.1",
63
68
  "@types/node": "26.2.0",
69
+ "@types/react": "19.2.7",
64
70
  "@vitest/coverage-v8": "4.1.11",
71
+ "react": "19.2.8",
65
72
  "tsdown": "0.22.14",
66
73
  "typescript": "7.0.2",
67
74
  "vitest": "4.1.11"
68
75
  },
69
76
  "peerDependencies": {
70
- "@earendil-works/pi-coding-agent": "0.84.2",
71
- "@earendil-works/pi-tui": "0.84.2"
77
+ "@earendil-works/pi-coding-agent": "0.84.3",
78
+ "@earendil-works/pi-tui": "0.84.3"
72
79
  },
73
80
  "peerDependenciesMeta": {
74
81
  "@earendil-works/pi-coding-agent": {
@@ -81,6 +88,11 @@
81
88
  "engines": {
82
89
  "node": ">=22.19.0"
83
90
  },
91
+ "doompiWeb": {
92
+ "pluginId": "ask-user",
93
+ "channels": [],
94
+ "client": "./web/index.ts"
95
+ },
84
96
  "pi": {
85
97
  "extensions": [
86
98
  "./dist/extensions/pi.mjs"
@@ -89,7 +101,7 @@
89
101
  "scripts": {
90
102
  "build": "tsdown",
91
103
  "test": "vitest --run",
92
- "typecheck": "tsc --noEmit",
104
+ "typecheck": "tsc --noEmit && tsc --noEmit -p web/tsconfig.json",
93
105
  "lint": "oxlint . && oxfmt . --check",
94
106
  "fixcode": "oxlint . --fix && oxfmt ."
95
107
  }
@@ -0,0 +1,85 @@
1
+ import {
2
+ MessageItem,
3
+ MessageItemBody,
4
+ MessageItemHeader,
5
+ MessageItemStatus,
6
+ toolTone,
7
+ } from '@agimon-ai/doompi-web-components';
8
+ import type { ToolMessageRenderProps } from '@agimon-ai/doompi-web-contracts';
9
+ import { askCallSummary, askResultView } from './askUserText.ts';
10
+
11
+ /** The answered list, or the outcome that stood in for it. */
12
+ function AskUserQuestionBody({ result, output, running, isError }: ToolMessageRenderProps) {
13
+ if (running) {
14
+ return (
15
+ <MessageItemBody data-testid="tool-result-ask_user_question">
16
+ <MessageItemStatus tone="running">waiting for the user</MessageItemStatus>
17
+ </MessageItemBody>
18
+ );
19
+ }
20
+ const view = askResultView(result?.details, output);
21
+ if (view.kind === 'cancelled') {
22
+ return (
23
+ <MessageItemBody data-testid="tool-result-ask_user_question">
24
+ <MessageItemStatus tone="running">cancelled</MessageItemStatus>
25
+ </MessageItemBody>
26
+ );
27
+ }
28
+ if (view.kind === 'voice') {
29
+ return (
30
+ <MessageItemBody data-testid="tool-result-ask_user_question">
31
+ <pre className="whitespace-pre-wrap break-words text-doom-dim">{view.prompt}</pre>
32
+ </MessageItemBody>
33
+ );
34
+ }
35
+ if (view.kind === 'text') {
36
+ return (
37
+ <MessageItemBody data-testid="tool-result-ask_user_question">
38
+ {view.text ? (
39
+ <pre className={`whitespace-pre-wrap break-words ${isError ? 'text-doom-red' : 'text-doom-dim'}`}>
40
+ {view.text}
41
+ </pre>
42
+ ) : (
43
+ <MessageItemStatus tone={isError ? 'error' : 'ok'}>{isError ? 'failed' : 'submitted'}</MessageItemStatus>
44
+ )}
45
+ </MessageItemBody>
46
+ );
47
+ }
48
+ return (
49
+ <MessageItemBody data-testid="tool-result-ask_user_question">
50
+ {view.answers.length === 0 ? (
51
+ <MessageItemStatus tone="ok">submitted</MessageItemStatus>
52
+ ) : (
53
+ <ul className="flex flex-col gap-1">
54
+ {view.answers.map((answer, index) => (
55
+ <li key={`${String(index)}-${answer.question}`} className="break-words">
56
+ <span className="text-doom-green">✓</span> <span className="text-doom-cyan">{answer.question}</span>:{' '}
57
+ <span className="text-doom-dim">{answer.answer}</span>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ )}
62
+ </MessageItemBody>
63
+ );
64
+ }
65
+
66
+ /**
67
+ * The ask_user_question tool's timeline item: "N question(s) (headers)" in
68
+ * the header, the web half of the TUI renderCall, and the answered list or
69
+ * the outcome that stood in for it in the body. The live questionnaire
70
+ * arrives as an extension-UI dialog; this is the transcript record.
71
+ */
72
+ export function AskUserQuestionToolMessage(props: ToolMessageRenderProps) {
73
+ const summary = askCallSummary(props.args);
74
+ return (
75
+ <MessageItem tone={toolTone({ running: props.running, isError: props.isError })}>
76
+ <MessageItemHeader title="ask_user_question">
77
+ <span data-testid="tool-call-ask_user_question" className="flex min-w-0 flex-1 items-center gap-2">
78
+ <span className="text-doom-hi">{summary.count}</span>
79
+ {summary.headers ? <span className="min-w-0 truncate text-doom-faint">({summary.headers})</span> : null}
80
+ </span>
81
+ </MessageItemHeader>
82
+ <AskUserQuestionBody {...props} />
83
+ </MessageItem>
84
+ );
85
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure text shaping for the ask_user_question card, apart from React so the
3
+ * unit suite covers it without a DOM. Args and details are wire JSON; the
4
+ * shapes narrowed here mirror src/types/questionnaire.ts, which this browser
5
+ * module may not import at runtime.
6
+ */
7
+
8
+ type Args = Readonly<Record<string, unknown>>;
9
+
10
+ function isRecord(value: unknown): value is Record<string, unknown> {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+
14
+ function asString(value: unknown): string {
15
+ return typeof value === 'string' ? value : '';
16
+ }
17
+
18
+ export interface AskCallSummary {
19
+ /** "N question(s)", the TUI heading's purpose. */
20
+ count: string;
21
+ /** The question headers, comma separated; empty when none carry one. */
22
+ headers: string;
23
+ }
24
+
25
+ /** The call header: how many questions, and their headers in parentheses. */
26
+ export function askCallSummary(args: Args): AskCallSummary {
27
+ const questions = Array.isArray(args.questions) ? args.questions.filter(isRecord) : [];
28
+ const headers = questions.map((question) => asString(question.header)).filter(Boolean);
29
+ return {
30
+ count: `${questions.length} question${questions.length === 1 ? '' : 's'}`,
31
+ headers: headers.join(', '),
32
+ };
33
+ }
34
+
35
+ export interface AnsweredQuestion {
36
+ question: string;
37
+ answer: string;
38
+ }
39
+
40
+ export type AskResultView =
41
+ | { kind: 'cancelled' }
42
+ | { kind: 'voice'; prompt: string }
43
+ | { kind: 'answers'; answers: AnsweredQuestion[] }
44
+ | { kind: 'text'; text: string };
45
+
46
+ /**
47
+ * The result body the TUI's renderResult chooses: a cancelled glyph, the
48
+ * voice prompt when the questionnaire was handed to voice, the answered
49
+ * list, or the raw text when the tool attached no details (an error path).
50
+ */
51
+ export function askResultView(details: unknown, output: string): AskResultView {
52
+ if (!isRecord(details) || !Array.isArray(details.answers)) return { kind: 'text', text: output };
53
+ if (details.cancelled === true) return { kind: 'cancelled' };
54
+ if (details.delivery === 'voice' && typeof details.voicePrompt === 'string') {
55
+ return { kind: 'voice', prompt: details.voicePrompt };
56
+ }
57
+ const answers = details.answers.filter(isRecord).map((answer) => ({
58
+ question: asString(answer.question),
59
+ answer:
60
+ answer.kind === 'multi'
61
+ ? (Array.isArray(answer.selected) ? answer.selected : []).filter((v) => typeof v === 'string').join(', ')
62
+ : asString(answer.answer),
63
+ }));
64
+ return { kind: 'answers', answers };
65
+ }
package/web/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineWebPlugin } from '@agimon-ai/doompi-web-contracts';
2
+ import { AskUserQuestionToolMessage } from './AskUserQuestionToolMessage.tsx';
3
+
4
+ /**
5
+ * This package's cockpit presence: the timeline card for ask_user_question,
6
+ * the web half of the TUI's renderCall and renderResult. The questionnaire
7
+ * itself reaches the cockpit as an extension UI dialog; this card is its
8
+ * record in the transcript.
9
+ */
10
+ export const webPlugin = defineWebPlugin({
11
+ id: 'ask-user',
12
+ toolRenderers: [{ tools: ['ask_user_question'], message: AskUserQuestionToolMessage }],
13
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noEmit": true,
15
+ "resolveJsonModule": true,
16
+ "esModuleInterop": true,
17
+ "skipLibCheck": true,
18
+ "allowImportingTsExtensions": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "types": ["node"]
21
+ },
22
+ "include": [".", "../src/types"]
23
+ }