@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 +1 -1
- package/package.json +20 -8
- package/web/AskUserQuestionToolMessage.tsx +85 -0
- package/web/askUserText.ts +65 -0
- package/web/index.ts +13 -0
- package/web/tsconfig.json +23 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-user-feedback",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
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-
|
|
58
|
-
"@agimon-ai/doompi-
|
|
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.
|
|
62
|
-
"@earendil-works/pi-tui": "0.84.
|
|
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.
|
|
71
|
-
"@earendil-works/pi-tui": "0.84.
|
|
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
|
+
}
|