@aphrody/m3-ai 3.3.2 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +29 -7
  2. package/dist/A2APeerInspector.d.ts +41 -0
  3. package/dist/A2APeerInspector.d.ts.map +1 -0
  4. package/dist/A2ATaskTimeline.d.ts +28 -0
  5. package/dist/A2ATaskTimeline.d.ts.map +1 -0
  6. package/dist/AgUiEventStream.d.ts +87 -0
  7. package/dist/AgUiEventStream.d.ts.map +1 -0
  8. package/dist/GeminiAccountMenu.d.ts +55 -0
  9. package/dist/GeminiAccountMenu.d.ts.map +1 -0
  10. package/dist/GeminiPersonalContextEdit.d.ts +29 -0
  11. package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
  12. package/dist/GoogleSpotlightBar.d.ts +32 -0
  13. package/dist/GoogleSpotlightBar.d.ts.map +1 -0
  14. package/dist/Markdown.d.ts +2 -1
  15. package/dist/Markdown.d.ts.map +1 -1
  16. package/dist/McpAppCard.d.ts +24 -0
  17. package/dist/McpAppCard.d.ts.map +1 -0
  18. package/dist/ModelSelector.d.ts.map +1 -1
  19. package/dist/TaskRow.d.ts.map +1 -1
  20. package/dist/ToolCall.d.ts.map +1 -1
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3629 -1950
  24. package/dist/index.js.map +32 -1
  25. package/package.json +17 -20
  26. package/src/A2APeerInspector.tsx +370 -0
  27. package/src/A2ATaskTimeline.tsx +218 -0
  28. package/src/AgUiEventStream.ts +492 -0
  29. package/src/ChatComposer.tsx +261 -0
  30. package/src/ConversationList.tsx +207 -0
  31. package/src/GeminiAccountMenu.tsx +212 -0
  32. package/src/GeminiPersonalContextEdit.tsx +147 -0
  33. package/src/GoogleSpotlightBar.tsx +165 -0
  34. package/src/InlineBranchCard.tsx +97 -0
  35. package/src/InlineWorkerCard.tsx +182 -0
  36. package/src/Markdown.tsx +769 -0
  37. package/src/McpAppCard.tsx +161 -0
  38. package/src/MemoryPanel.tsx +176 -0
  39. package/src/MessageBubble.tsx +143 -0
  40. package/src/ModelSelector.tsx +319 -0
  41. package/src/ModelUnavailable.tsx +116 -0
  42. package/src/TaskCreateForm.tsx +123 -0
  43. package/src/TaskDetail.tsx +224 -0
  44. package/src/TaskList.tsx +136 -0
  45. package/src/TaskPriorityIcon.tsx +39 -0
  46. package/src/TaskRow.tsx +177 -0
  47. package/src/TaskStatusIcon.tsx +39 -0
  48. package/src/ToolCall.tsx +644 -0
  49. package/src/chatIcons.tsx +105 -0
  50. package/src/chatLabels.ts +243 -0
  51. package/src/citations.tsx +286 -0
  52. package/src/index.ts +173 -0
  53. package/src/internal.tsx +107 -0
  54. package/src/openaiChatStream.ts +400 -0
  55. package/src/types.ts +212 -0
  56. package/src/useOpenAIChat.ts +126 -0
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * @aphrody/m3-ai - McpAppCard
4
+ *
5
+ * Material Design 3 Interactive MCP App Container:
6
+ * - Renders generative UI resources produced by MCP tools (SEP-1724 / mcp-ui).
7
+ * - Hosts interactive guest web applications in an isolated sandbox iframe.
8
+ * - Passes reactive M3 theme and color scheme context to the guest app.
9
+ * - Supports expandable full-bleed or embedded card layouts.
10
+ */
11
+
12
+ import React, { useState, useRef, useMemo } from "react";
13
+ import { clsx } from "clsx";
14
+ import { AppRenderer, type SandboxConfig, type McpUiHostContext } from "@aphrody/mcp-ui";
15
+
16
+ export interface McpAppCardProps {
17
+ toolName: string;
18
+ title?: string;
19
+ html?: string;
20
+ toolResourceUri?: string;
21
+ toolInput?: Record<string, unknown>;
22
+ toolResult?: any;
23
+ sandboxUrl?: string;
24
+ isExpandedDefault?: boolean;
25
+ className?: string;
26
+ onAction?: (action: string, payload?: unknown) => void;
27
+ }
28
+
29
+ export const McpAppCard: React.FC<McpAppCardProps> = ({
30
+ toolName,
31
+ title,
32
+ html,
33
+ toolResourceUri,
34
+ toolInput,
35
+ toolResult,
36
+ sandboxUrl,
37
+ isExpandedDefault = false,
38
+ className,
39
+ onAction: _onAction,
40
+ }) => {
41
+ const [isExpanded, setIsExpanded] = useState(isExpandedDefault);
42
+ const [hasError, setHasError] = useState<string | null>(null);
43
+ const containerRef = useRef<HTMLDivElement>(null);
44
+
45
+ // Host context with Material Design 3 tokens
46
+ const hostContext: McpUiHostContext = useMemo(() => {
47
+ const isDark =
48
+ typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
49
+ return {
50
+ theme: isDark ? "dark" : "light",
51
+ locale: typeof navigator !== "undefined" ? navigator.language : "en-US",
52
+ displayMode: isExpanded ? "fullscreen" : "inline",
53
+ };
54
+ }, [isExpanded]);
55
+
56
+ // Fallback default sandbox proxy URL
57
+ const resolvedSandbox: SandboxConfig = useMemo(() => {
58
+ const url = sandboxUrl ? new URL(sandboxUrl) : new URL("https://mcpui.dev/sandbox_proxy.html");
59
+ return {
60
+ url,
61
+ permissions: "allow-scripts allow-forms allow-same-origin",
62
+ };
63
+ }, [sandboxUrl]);
64
+
65
+ return (
66
+ <div
67
+ ref={containerRef}
68
+ className={clsx(
69
+ "flex flex-col rounded-3xl border transition-all duration-300 overflow-hidden",
70
+ "bg-[var(--md-sys-color-surface-container-low,#f7f2fa)]",
71
+ "border-[var(--md-sys-color-outline-variant,#cac4d0)]",
72
+ "text-[var(--md-sys-color-on-surface,#1d1b20)]",
73
+ isExpanded ? "fixed inset-4 z-50 shadow-2xl" : "relative w-full shadow-sm",
74
+ className,
75
+ )}
76
+ >
77
+ {/* App Card Header */}
78
+ <div className="flex items-center justify-between px-4 py-3 border-b border-[var(--md-sys-color-outline-variant,#cac4d0)]/40 bg-[var(--md-sys-color-surface-container,#f3edf7)]/60">
79
+ <div className="flex items-center gap-2.5">
80
+ <div className="flex h-7 w-7 items-center justify-center rounded-xl bg-[var(--md-sys-color-primary-container,#eaddff)] text-[var(--md-sys-color-on-primary-container,#21005d)]">
81
+ <span className="material-symbols-outlined text-base">widgets</span>
82
+ </div>
83
+
84
+ <div className="flex flex-col">
85
+ <div className="flex items-center gap-2">
86
+ <span className="text-xs font-semibold tracking-tight">{title || toolName}</span>
87
+ <span className="rounded-full bg-[var(--md-sys-color-surface-container-high,#ece6f0)] px-2 py-0.2 text-[10px] font-mono text-[var(--md-sys-color-on-surface-variant,#49454f)]">
88
+ mcp-ui
89
+ </span>
90
+ </div>
91
+ {toolResourceUri && (
92
+ <span className="text-[10px] font-mono text-[var(--md-sys-color-outline,#79747e)]">
93
+ {toolResourceUri}
94
+ </span>
95
+ )}
96
+ </div>
97
+ </div>
98
+
99
+ {/* Action icons */}
100
+ <div className="flex items-center gap-1">
101
+ <div className="flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-700 dark:text-emerald-400">
102
+ <span className="material-symbols-outlined text-xs">verified_user</span>
103
+ <span>Sandboxed</span>
104
+ </div>
105
+
106
+ <button
107
+ type="button"
108
+ onClick={() => setIsExpanded(!isExpanded)}
109
+ title={isExpanded ? "Collapse" : "Expand Fullscreen"}
110
+ className="flex h-7 w-7 items-center justify-center rounded-lg text-[var(--md-sys-color-on-surface-variant,#49454f)] hover:bg-[var(--md-sys-color-surface-container-high,#ece6f0)] transition-colors"
111
+ >
112
+ <span className="material-symbols-outlined text-base">
113
+ {isExpanded ? "close_fullscreen" : "open_in_full"}
114
+ </span>
115
+ </button>
116
+ </div>
117
+ </div>
118
+
119
+ {/* App Frame Content */}
120
+ <div
121
+ className={clsx(
122
+ "relative w-full overflow-hidden transition-all",
123
+ isExpanded ? "flex-1 min-h-[500px]" : "h-96",
124
+ )}
125
+ >
126
+ {hasError ? (
127
+ <div className="flex h-full flex-col items-center justify-center p-6 text-center text-xs text-[var(--md-sys-color-error,#b3261e)]">
128
+ <span className="material-symbols-outlined text-3xl mb-1">error</span>
129
+ <span className="font-semibold">MCP App Render Error</span>
130
+ <p className="mt-1 font-mono text-[11px] opacity-80">{hasError}</p>
131
+ </div>
132
+ ) : html ? (
133
+ <AppRenderer
134
+ toolName={toolName}
135
+ html={html}
136
+ sandbox={resolvedSandbox}
137
+ toolInput={toolInput}
138
+ toolResult={toolResult}
139
+ hostContext={hostContext}
140
+ onError={(err) => setHasError(err.message)}
141
+ />
142
+ ) : (
143
+ <div className="flex h-full flex-col items-center justify-center p-6 text-xs text-[var(--md-sys-color-outline,#79747e)]">
144
+ <span className="material-symbols-outlined text-2xl animate-spin mb-1 text-[var(--md-sys-color-primary,#6750a4)]">
145
+ progress_activity
146
+ </span>
147
+ <span>Awaiting MCP UI Resource ({toolName})...</span>
148
+ </div>
149
+ )}
150
+ </div>
151
+
152
+ {/* Footer info */}
153
+ {toolInput && (
154
+ <div className="flex items-center justify-between px-4 py-2 border-t border-[var(--md-sys-color-outline-variant,#cac4d0)]/30 bg-[var(--md-sys-color-surface-container-lowest,#ffffff)] text-[10px] text-[var(--md-sys-color-outline,#79747e)] font-mono">
155
+ <span className="truncate">input: {JSON.stringify(toolInput)}</span>
156
+ <span className="shrink-0 ml-2">M3 UI Bridge</span>
157
+ </div>
158
+ )}
159
+ </div>
160
+ );
161
+ };
@@ -0,0 +1,176 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Memory panel: the agent's saved memories as an M3 list, with per-item deletion and a
3
+ // "clear all" action guarded by an inline confirmation (or an injected one).
4
+
5
+ "use client";
6
+
7
+ import { CircleButton } from "@aphrody/m3-primitives";
8
+ import { clsx } from "clsx";
9
+ import { useMemo, useState } from "react";
10
+
11
+ import { type ChatLabels, resolveChatLabels } from "./chatLabels";
12
+ import { ChatIcon } from "./chatIcons";
13
+ import { CircularIndicator, focusRing } from "./internal";
14
+
15
+ export interface MemoryItem {
16
+ id: string;
17
+ content: string;
18
+ tags?: string[];
19
+ /** ISO string, epoch milliseconds or Date. */
20
+ createdAt: string | number | Date;
21
+ }
22
+
23
+ export interface MemoryPanelProps {
24
+ memories: readonly MemoryItem[];
25
+ loading?: boolean;
26
+ onDelete?: (id: string) => void;
27
+ /** Shows "clear all". */
28
+ onClear?: () => void;
29
+ /**
30
+ * Replaces the built-in inline confirmation of "clear all" (e.g. an app dialog):
31
+ * resolve `true` to clear.
32
+ */
33
+ confirmClear?: () => boolean | Promise<boolean>;
34
+ labels?: Partial<ChatLabels>;
35
+ locale?: string;
36
+ formatDate?: (createdAt: Date) => string;
37
+ className?: string;
38
+ }
39
+
40
+ const textButton = clsx(
41
+ "inline-flex h-10 items-center rounded-full px-3 text-label-large",
42
+ "transition-colors duration-200 ease-standard",
43
+ focusRing,
44
+ );
45
+
46
+ export function MemoryPanel({
47
+ memories,
48
+ loading = false,
49
+ onDelete,
50
+ onClear,
51
+ confirmClear,
52
+ labels,
53
+ locale,
54
+ formatDate,
55
+ className,
56
+ }: MemoryPanelProps) {
57
+ const l = resolveChatLabels(labels);
58
+ const [confirming, setConfirming] = useState(false);
59
+ const dateFormat = useMemo(
60
+ () => new Intl.DateTimeFormat(locale, { dateStyle: "medium" }),
61
+ [locale],
62
+ );
63
+ const format = (value: string | number | Date) => {
64
+ const date = value instanceof Date ? value : new Date(value);
65
+ if (Number.isNaN(date.getTime())) return "";
66
+ return formatDate ? formatDate(date) : dateFormat.format(date);
67
+ };
68
+
69
+ const requestClear = async () => {
70
+ if (!onClear) return;
71
+ if (confirmClear) {
72
+ if (await confirmClear()) onClear();
73
+ return;
74
+ }
75
+ setConfirming(true);
76
+ };
77
+
78
+ return (
79
+ <section
80
+ aria-label={l.memory}
81
+ aria-busy={loading || undefined}
82
+ className={clsx(
83
+ "m3-memory-panel flex min-h-0 flex-col gap-2 rounded-large bg-surface-container-low py-3 text-on-surface",
84
+ className,
85
+ )}
86
+ >
87
+ <header className="flex items-center justify-between gap-2 px-4">
88
+ <h2 className="m-0 text-title-medium">{l.memory}</h2>
89
+ {onClear && memories.length > 0 && !confirming ? (
90
+ <button
91
+ type="button"
92
+ onClick={() => void requestClear()}
93
+ className={clsx(textButton, "text-error hover:bg-error/8")}
94
+ >
95
+ {l.clearMemory}
96
+ </button>
97
+ ) : null}
98
+ </header>
99
+ {confirming ? (
100
+ <div
101
+ role="alertdialog"
102
+ aria-label={l.clearMemory}
103
+ className="mx-4 flex flex-col gap-2 rounded-medium bg-error-container px-4 py-3 text-on-error-container"
104
+ >
105
+ <p className="m-0 text-body-medium">{l.clearMemoryConfirm}</p>
106
+ <div className="flex justify-end gap-2">
107
+ <button
108
+ type="button"
109
+ onClick={() => setConfirming(false)}
110
+ className={clsx(textButton, "hover:bg-on-error-container/8")}
111
+ >
112
+ {l.cancel}
113
+ </button>
114
+ <button
115
+ type="button"
116
+ onClick={() => {
117
+ setConfirming(false);
118
+ onClear?.();
119
+ }}
120
+ className={clsx(textButton, "bg-error px-4 text-on-error hover:bg-error/92")}
121
+ >
122
+ {l.confirm}
123
+ </button>
124
+ </div>
125
+ </div>
126
+ ) : null}
127
+ {loading ? (
128
+ <div className="flex justify-center py-6">
129
+ <CircularIndicator size={24} label={l.memoryLoading} />
130
+ </div>
131
+ ) : memories.length === 0 ? (
132
+ <p className="m-0 px-4 py-6 text-center text-body-medium text-on-surface-variant">
133
+ {l.memoryEmpty}
134
+ </p>
135
+ ) : (
136
+ <ul className="m-0 flex min-h-0 list-none flex-col overflow-y-auto p-0">
137
+ {memories.map((memory) => (
138
+ <li
139
+ key={memory.id}
140
+ className="group flex items-start gap-3 px-4 py-2 transition-colors duration-200 ease-standard hover:bg-on-surface/8"
141
+ >
142
+ <div className="min-w-0 flex-1">
143
+ <p className="m-0 text-body-medium break-words whitespace-pre-wrap text-on-surface">
144
+ {memory.content}
145
+ </p>
146
+ <div className="mt-1 flex flex-wrap items-center gap-1.5">
147
+ <time className="text-label-small text-on-surface-variant">
148
+ {format(memory.createdAt)}
149
+ </time>
150
+ {memory.tags?.map((tag) => (
151
+ <span
152
+ key={tag}
153
+ className="inline-flex h-6 items-center rounded-small border border-outline-variant px-2 text-label-small text-on-surface-variant"
154
+ >
155
+ {tag}
156
+ </span>
157
+ ))}
158
+ </div>
159
+ </div>
160
+ {onDelete ? (
161
+ <CircleButton
162
+ icon={<ChatIcon name="delete" size={18} />}
163
+ variant="standard"
164
+ size="sm"
165
+ onClick={() => onDelete(memory.id)}
166
+ aria-label={l.deleteMemory}
167
+ title={l.deleteMemory}
168
+ />
169
+ ) : null}
170
+ </li>
171
+ ))}
172
+ </ul>
173
+ )}
174
+ </section>
175
+ );
176
+ }
@@ -0,0 +1,143 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/MessageBubble.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ "use client";
5
+
6
+ import { CircleButton } from "@aphrody/m3-primitives";
7
+ import { clsx } from "clsx";
8
+
9
+ import { type ChatLabels, resolveChatLabels } from "./chatLabels";
10
+ import { type Citation, CitationList, CitedMarkdown, RefusalNotice } from "./citations";
11
+ import { ChatIcon } from "./chatIcons";
12
+ import { Markdown, type MarkdownLinkHandler } from "./Markdown";
13
+
14
+ export interface MessageBubbleProps {
15
+ content: string;
16
+ isUser: boolean;
17
+ isStreaming?: boolean;
18
+ onCopy?: (content: string) => void;
19
+ onDislike?: (content: string) => void;
20
+ onFork?: () => void;
21
+ variant?: "default" | "codex";
22
+ className?: string;
23
+ /** Visible strings (aria-labels, titles); English defaults. */
24
+ labels?: Partial<ChatLabels>;
25
+ /** Grounded answer: `[n]` markers become citation chips and the sources are listed below. */
26
+ citations?: readonly Citation[];
27
+ /** The backend refused to answer: a refusal notice replaces an empty answer. */
28
+ refusal?: boolean;
29
+ onCitationSelect?: (citation: Citation) => void;
30
+ /** Link activation in the answer (e.g. open the system browser from Tauri). */
31
+ onLinkClick?: MarkdownLinkHandler;
32
+ }
33
+
34
+ /**
35
+ * One chat turn. The user turn is an M3 bubble, the assistant turn is plain markdown
36
+ * on the surface, as in M3 / Codex conversations.
37
+ */
38
+ export function MessageBubble({
39
+ content,
40
+ isUser,
41
+ isStreaming = false,
42
+ onCopy,
43
+ onDislike,
44
+ onFork,
45
+ variant = "default",
46
+ className,
47
+ labels,
48
+ citations,
49
+ refusal = false,
50
+ onCitationSelect,
51
+ onLinkClick,
52
+ }: MessageBubbleProps) {
53
+ const isCodex = variant === "codex";
54
+ const l = resolveChatLabels(labels);
55
+ const cited = !isUser && citations && citations.length > 0 ? citations : null;
56
+
57
+ return (
58
+ <div
59
+ className={clsx(
60
+ "group flex flex-col py-1.5 w-full",
61
+ isUser ? "items-end" : "items-start",
62
+ className,
63
+ )}
64
+ >
65
+ <div
66
+ className={clsx(
67
+ "min-w-0",
68
+ isUser
69
+ ? isCodex
70
+ ? "max-w-[75%] rounded-[28px] bg-surface-container-high px-5 py-3 text-body-large text-on-surface shadow-xs"
71
+ : "max-w-[80%] rounded-large rounded-br-extra-small bg-secondary-container px-4 py-2.5 text-body-large text-on-secondary-container"
72
+ : "w-full max-w-full bg-transparent text-body-large text-on-surface",
73
+ )}
74
+ aria-busy={isStreaming || undefined}
75
+ >
76
+ {isUser ? (
77
+ <div className="whitespace-pre-wrap break-words">{content}</div>
78
+ ) : (
79
+ <>
80
+ {refusal && !content.trim() ? (
81
+ <RefusalNotice labels={labels} />
82
+ ) : cited ? (
83
+ <CitedMarkdown
84
+ content={content}
85
+ citations={cited}
86
+ labels={labels}
87
+ onCitationSelect={onCitationSelect}
88
+ onLinkClick={onLinkClick}
89
+ className="break-words font-sans"
90
+ />
91
+ ) : (
92
+ <Markdown
93
+ content={content}
94
+ size="large"
95
+ labels={labels}
96
+ onLinkClick={onLinkClick}
97
+ className="break-words font-sans"
98
+ />
99
+ )}
100
+ {refusal && content.trim() ? <RefusalNotice labels={labels} className="mt-2" /> : null}
101
+ {cited && !isStreaming ? (
102
+ <CitationList citations={cited} labels={labels} onSelect={onCitationSelect} />
103
+ ) : null}
104
+ </>
105
+ )}
106
+ </div>
107
+ {!isUser && (onCopy || onDislike || onFork) ? (
108
+ <div className="mt-1.5 flex items-center gap-1 opacity-0 transition-opacity duration-200 ease-standard group-focus-within:opacity-100 group-hover:opacity-100">
109
+ {onCopy && (
110
+ <CircleButton
111
+ icon={<ChatIcon name="content_copy" size={18} />}
112
+ variant="standard"
113
+ size="sm"
114
+ onClick={() => onCopy(content)}
115
+ aria-label={isStreaming ? l.copyStreamingMessage : l.copyMessage}
116
+ title={isStreaming ? l.copyStreamingMessage : l.copyMessage}
117
+ />
118
+ )}
119
+ {onDislike && (
120
+ <CircleButton
121
+ icon={<ChatIcon name="thumb_down" size={18} />}
122
+ variant="standard"
123
+ size="sm"
124
+ onClick={() => onDislike(content)}
125
+ aria-label={l.dislike}
126
+ title={l.dislike}
127
+ />
128
+ )}
129
+ {onFork && (
130
+ <CircleButton
131
+ icon={<ChatIcon name="call_split" size={18} />}
132
+ variant="standard"
133
+ size="sm"
134
+ onClick={onFork}
135
+ aria-label={l.fork}
136
+ title={l.fork}
137
+ />
138
+ )}
139
+ </div>
140
+ ) : null}
141
+ </div>
142
+ );
143
+ }