@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.
- package/README.md +29 -7
- package/dist/A2APeerInspector.d.ts +41 -0
- package/dist/A2APeerInspector.d.ts.map +1 -0
- package/dist/A2ATaskTimeline.d.ts +28 -0
- package/dist/A2ATaskTimeline.d.ts.map +1 -0
- package/dist/AgUiEventStream.d.ts +87 -0
- package/dist/AgUiEventStream.d.ts.map +1 -0
- package/dist/GeminiAccountMenu.d.ts +55 -0
- package/dist/GeminiAccountMenu.d.ts.map +1 -0
- package/dist/GeminiPersonalContextEdit.d.ts +29 -0
- package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
- package/dist/GoogleSpotlightBar.d.ts +32 -0
- package/dist/GoogleSpotlightBar.d.ts.map +1 -0
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.d.ts.map +1 -1
- package/dist/McpAppCard.d.ts +24 -0
- package/dist/McpAppCard.d.ts.map +1 -0
- package/dist/ModelSelector.d.ts.map +1 -1
- package/dist/TaskRow.d.ts.map +1 -1
- package/dist/ToolCall.d.ts.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3629 -1950
- package/dist/index.js.map +32 -1
- package/package.json +17 -20
- package/src/A2APeerInspector.tsx +370 -0
- package/src/A2ATaskTimeline.tsx +218 -0
- package/src/AgUiEventStream.ts +492 -0
- package/src/ChatComposer.tsx +261 -0
- package/src/ConversationList.tsx +207 -0
- package/src/GeminiAccountMenu.tsx +212 -0
- package/src/GeminiPersonalContextEdit.tsx +147 -0
- package/src/GoogleSpotlightBar.tsx +165 -0
- package/src/InlineBranchCard.tsx +97 -0
- package/src/InlineWorkerCard.tsx +182 -0
- package/src/Markdown.tsx +769 -0
- package/src/McpAppCard.tsx +161 -0
- package/src/MemoryPanel.tsx +176 -0
- package/src/MessageBubble.tsx +143 -0
- package/src/ModelSelector.tsx +319 -0
- package/src/ModelUnavailable.tsx +116 -0
- package/src/TaskCreateForm.tsx +123 -0
- package/src/TaskDetail.tsx +224 -0
- package/src/TaskList.tsx +136 -0
- package/src/TaskPriorityIcon.tsx +39 -0
- package/src/TaskRow.tsx +177 -0
- package/src/TaskStatusIcon.tsx +39 -0
- package/src/ToolCall.tsx +644 -0
- package/src/chatIcons.tsx +105 -0
- package/src/chatLabels.ts +243 -0
- package/src/citations.tsx +286 -0
- package/src/index.ts +173 -0
- package/src/internal.tsx +107 -0
- package/src/openaiChatStream.ts +400 -0
- package/src/types.ts +212 -0
- package/src/useOpenAIChat.ts +126 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0 AND MIT
|
|
2
|
+
// Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/types.ts), MIT.
|
|
3
|
+
|
|
4
|
+
// Tool execution types
|
|
5
|
+
export type ToolCallStatus = "running" | "completed" | "error";
|
|
6
|
+
|
|
7
|
+
export interface ToolCallPair {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
argsRaw: string;
|
|
11
|
+
args: Record<string, unknown> | null;
|
|
12
|
+
resultRaw: string | null;
|
|
13
|
+
result: Record<string, unknown> | null;
|
|
14
|
+
status: ToolCallStatus;
|
|
15
|
+
/** Human-readable summary provided by live opencode parts */
|
|
16
|
+
title?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Transcript step types — discriminated union matching the real API schema
|
|
20
|
+
|
|
21
|
+
export type ActionContent =
|
|
22
|
+
| { type: "text"; text: string }
|
|
23
|
+
| { type: "tool_call"; id: string; name: string; args: string };
|
|
24
|
+
|
|
25
|
+
export type TranscriptStep =
|
|
26
|
+
| { type: "action"; content: ActionContent[] }
|
|
27
|
+
| { type: "user_text"; text: string }
|
|
28
|
+
| { type: "system_text"; text: string }
|
|
29
|
+
| { type: "tool_result"; call_id: string; name: string; text: string };
|
|
30
|
+
|
|
31
|
+
// Paired transcript item for rendering
|
|
32
|
+
|
|
33
|
+
export type TranscriptItem = { kind: "text"; text: string } | { kind: "tool"; pair: ToolCallPair };
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Task board types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export type TaskStatus = "pending_approval" | "backlog" | "ready" | "in_progress" | "done";
|
|
40
|
+
export type TaskPriority = "critical" | "high" | "medium" | "low";
|
|
41
|
+
|
|
42
|
+
export interface Subtask {
|
|
43
|
+
title: string;
|
|
44
|
+
completed: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Task {
|
|
48
|
+
id: string;
|
|
49
|
+
task_number: number;
|
|
50
|
+
title: string;
|
|
51
|
+
description?: string | null;
|
|
52
|
+
status: TaskStatus;
|
|
53
|
+
priority: TaskPriority;
|
|
54
|
+
owner_agent_id: string;
|
|
55
|
+
assigned_agent_id: string;
|
|
56
|
+
subtasks: Subtask[];
|
|
57
|
+
metadata: unknown;
|
|
58
|
+
worker_id?: string | null;
|
|
59
|
+
created_by: string;
|
|
60
|
+
created_at: string;
|
|
61
|
+
updated_at: string;
|
|
62
|
+
completed_at?: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const TASK_STATUS_ORDER: TaskStatus[] = [
|
|
66
|
+
"in_progress",
|
|
67
|
+
"ready",
|
|
68
|
+
"pending_approval",
|
|
69
|
+
"backlog",
|
|
70
|
+
"done",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export const TASK_STATUS_LABEL: Record<TaskStatus, string> = {
|
|
74
|
+
in_progress: "In Progress",
|
|
75
|
+
ready: "Ready",
|
|
76
|
+
pending_approval: "Pending Approval",
|
|
77
|
+
backlog: "Backlog",
|
|
78
|
+
done: "Done",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const TASK_PRIORITY_LABEL: Record<TaskPriority, string> = {
|
|
82
|
+
critical: "Critical",
|
|
83
|
+
high: "High",
|
|
84
|
+
medium: "Medium",
|
|
85
|
+
low: "Low",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Domain objects
|
|
89
|
+
export interface TaskInfo {
|
|
90
|
+
id: string;
|
|
91
|
+
title: string;
|
|
92
|
+
status: string;
|
|
93
|
+
priority: string;
|
|
94
|
+
assignees: string[];
|
|
95
|
+
conversation_id?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MemoryInfo {
|
|
99
|
+
id: string;
|
|
100
|
+
type: string;
|
|
101
|
+
content: string;
|
|
102
|
+
source?: string;
|
|
103
|
+
edges?: Array<{ target: string; relation: string }>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface CronJobInfo {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
schedule: string;
|
|
110
|
+
last_run?: string;
|
|
111
|
+
next_run?: string;
|
|
112
|
+
status: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface AgentInfo {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
detail: string;
|
|
119
|
+
status?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ModelOption {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
provider: string;
|
|
126
|
+
context_window?: number;
|
|
127
|
+
capabilities?: string[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Helpers
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
export function tryParseJson(text: string): Record<string, unknown> | null {
|
|
135
|
+
if (!text || text.trim().length === 0) return null;
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(text);
|
|
138
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
139
|
+
return parsed as Record<string, unknown>;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isErrorResult(text: string, parsed: Record<string, unknown> | null): boolean {
|
|
148
|
+
if (parsed?.error) return true;
|
|
149
|
+
if (parsed?.status === "error") return true;
|
|
150
|
+
if (parsed?.success === false) return true;
|
|
151
|
+
if (typeof parsed?.exit_code === "number" && parsed.exit_code !== 0) return true;
|
|
152
|
+
const lower = text.toLowerCase();
|
|
153
|
+
return (
|
|
154
|
+
lower.startsWith("error:") ||
|
|
155
|
+
lower.startsWith("error -") ||
|
|
156
|
+
lower.startsWith("failed:") ||
|
|
157
|
+
lower.startsWith("toolset error:")
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Transcript → TranscriptItem[] pairing
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Walk a flat TranscriptStep[] and pair each tool_call with its tool_result
|
|
167
|
+
* via call_id matching, plus emit standalone text steps. Returns an ordered
|
|
168
|
+
* list of renderable items: text blocks and paired tool calls.
|
|
169
|
+
*/
|
|
170
|
+
export function pairTranscriptSteps(steps: TranscriptStep[]): TranscriptItem[] {
|
|
171
|
+
const items: TranscriptItem[] = [];
|
|
172
|
+
const resultsById = new Map<string, { name: string; text: string }>();
|
|
173
|
+
|
|
174
|
+
// First pass: index all tool_result steps by call_id
|
|
175
|
+
for (const step of steps) {
|
|
176
|
+
if (step.type === "tool_result") {
|
|
177
|
+
resultsById.set(step.call_id, { name: step.name, text: step.text });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Second pass: emit items in order
|
|
182
|
+
for (const step of steps) {
|
|
183
|
+
if (step.type === "action") {
|
|
184
|
+
for (const content of step.content) {
|
|
185
|
+
if (content.type === "text") {
|
|
186
|
+
items.push({ kind: "text", text: content.text });
|
|
187
|
+
} else if (content.type === "tool_call") {
|
|
188
|
+
const result = resultsById.get(content.id);
|
|
189
|
+
const parsedArgs = tryParseJson(content.args);
|
|
190
|
+
const parsedResult = result ? tryParseJson(result.text) : null;
|
|
191
|
+
|
|
192
|
+
const hasError = result ? isErrorResult(result.text, parsedResult) : false;
|
|
193
|
+
|
|
194
|
+
items.push({
|
|
195
|
+
kind: "tool",
|
|
196
|
+
pair: {
|
|
197
|
+
id: content.id,
|
|
198
|
+
name: content.name,
|
|
199
|
+
argsRaw: content.args,
|
|
200
|
+
args: parsedArgs,
|
|
201
|
+
resultRaw: result?.text ?? null,
|
|
202
|
+
result: parsedResult,
|
|
203
|
+
status: result ? (hasError ? "error" : "completed") : "running",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return items;
|
|
212
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// React hook over `streamChatCompletion`: one conversation streamed from an OpenAI-compatible
|
|
3
|
+
// endpoint, with stop, reset and the `unavailable` status of a model that cannot answer.
|
|
4
|
+
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useReducer, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type ChatMessage,
|
|
11
|
+
type ChatStatus,
|
|
12
|
+
ChatStreamError,
|
|
13
|
+
chatReducer,
|
|
14
|
+
initialChatState,
|
|
15
|
+
newMessageId,
|
|
16
|
+
streamChatCompletion,
|
|
17
|
+
} from "./openaiChatStream";
|
|
18
|
+
|
|
19
|
+
export interface UseOpenAIChatOptions {
|
|
20
|
+
/** API root including `/v1` (e.g. `http://127.0.0.1:8080/v1`). */
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
initialMessages?: ChatMessage[];
|
|
26
|
+
/** Messages sent before the conversation on every request (system prompt...). */
|
|
27
|
+
preamble?: ChatMessage[];
|
|
28
|
+
/** Extra body fields (`temperature`, `tools`...). */
|
|
29
|
+
body?: Record<string, unknown>;
|
|
30
|
+
/** Called with the finished assistant message. */
|
|
31
|
+
onFinish?: (message: ChatMessage) => void;
|
|
32
|
+
onError?: (error: ChatStreamError) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseOpenAIChatResult {
|
|
36
|
+
messages: ChatMessage[];
|
|
37
|
+
status: ChatStatus;
|
|
38
|
+
error: ChatStreamError | null;
|
|
39
|
+
/** Send a user message and stream the answer. Ignored while streaming. */
|
|
40
|
+
send: (content: string) => Promise<void>;
|
|
41
|
+
/** Abort the running request; the partial answer stays. */
|
|
42
|
+
stop: () => void;
|
|
43
|
+
/** Abort and clear (or replace) the conversation. */
|
|
44
|
+
reset: (messages?: ChatMessage[]) => void;
|
|
45
|
+
setMessages: (messages: ChatMessage[]) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useOpenAIChat(options: UseOpenAIChatOptions): UseOpenAIChatResult {
|
|
49
|
+
const [state, dispatch] = useReducer(chatReducer, options.initialMessages, initialChatState);
|
|
50
|
+
const controller = useRef<AbortController | null>(null);
|
|
51
|
+
const latest = useRef({ options, state });
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
latest.current = { options, state };
|
|
55
|
+
});
|
|
56
|
+
useEffect(() => () => controller.current?.abort(), []);
|
|
57
|
+
|
|
58
|
+
const send = useCallback(async (content: string) => {
|
|
59
|
+
const { options: o, state: s } = latest.current;
|
|
60
|
+
if (s.status === "streaming" || controller.current || !content.trim()) return;
|
|
61
|
+
const user: ChatMessage = { id: newMessageId(), role: "user", content };
|
|
62
|
+
const assistantId = newMessageId();
|
|
63
|
+
const abort = new AbortController();
|
|
64
|
+
controller.current = abort;
|
|
65
|
+
dispatch({ type: "start", user, assistantId });
|
|
66
|
+
let assistant: ChatMessage = { id: assistantId, role: "assistant", content: "" };
|
|
67
|
+
try {
|
|
68
|
+
for await (const event of streamChatCompletion({
|
|
69
|
+
baseUrl: o.baseUrl,
|
|
70
|
+
model: o.model,
|
|
71
|
+
messages: [...(o.preamble ?? []), ...s.messages, user],
|
|
72
|
+
body: o.body,
|
|
73
|
+
headers: o.headers,
|
|
74
|
+
fetch: o.fetch,
|
|
75
|
+
signal: abort.signal,
|
|
76
|
+
})) {
|
|
77
|
+
dispatch({ type: "event", assistantId, event });
|
|
78
|
+
assistant = chatReducer(
|
|
79
|
+
{ messages: [assistant], status: "streaming", error: null },
|
|
80
|
+
{ type: "event", assistantId, event },
|
|
81
|
+
).messages[0]!;
|
|
82
|
+
}
|
|
83
|
+
dispatch({ type: "finish" });
|
|
84
|
+
o.onFinish?.(assistant);
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
const error =
|
|
87
|
+
cause instanceof ChatStreamError
|
|
88
|
+
? cause
|
|
89
|
+
: new ChatStreamError("stream", String(cause), { cause });
|
|
90
|
+
if (error.kind === "aborted" || abort.signal.aborted) {
|
|
91
|
+
dispatch({ type: "stop" });
|
|
92
|
+
} else {
|
|
93
|
+
dispatch({ type: "fail", error });
|
|
94
|
+
o.onError?.(error);
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
if (controller.current === abort) controller.current = null;
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const stop = useCallback(() => {
|
|
102
|
+
controller.current?.abort();
|
|
103
|
+
controller.current = null;
|
|
104
|
+
dispatch({ type: "stop" });
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const reset = useCallback((messages?: ChatMessage[]) => {
|
|
108
|
+
controller.current?.abort();
|
|
109
|
+
controller.current = null;
|
|
110
|
+
dispatch({ type: "reset", messages });
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const setMessages = useCallback((messages: ChatMessage[]) => {
|
|
114
|
+
dispatch({ type: "set", messages });
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
messages: state.messages,
|
|
119
|
+
status: state.status,
|
|
120
|
+
error: state.error,
|
|
121
|
+
send,
|
|
122
|
+
stop,
|
|
123
|
+
reset,
|
|
124
|
+
setMessages,
|
|
125
|
+
};
|
|
126
|
+
}
|