@agents24/chat-react 0.5.30 → 0.5.31
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 +9 -4
- package/dist/generated/delegation-types.d.ts +129 -0
- package/dist/registry/agents24-chat.json +37 -19
- package/dist/runtime/index.cjs +52 -0
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +51 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/use-delegation-tasks.d.ts +20 -0
- package/dist/scaffold/app.d.ts +2 -1
- package/dist/scaffold/components/chat/chat-composer.d.ts +4 -1
- package/dist/scaffold/components/chat/subagent-tasks.d.ts +3 -0
- package/dist/scaffold/components/theme-provider.d.ts +4 -3
- package/dist/scaffold/index.cjs +425 -280
- package/dist/scaffold/index.cjs.map +1 -1
- package/dist/scaffold/index.d.ts +3 -2
- package/dist/scaffold/index.js +416 -271
- package/dist/scaffold/index.js.map +1 -1
- package/dist/styles.css +14 -0
- package/dist/ui/adapters.d.ts +5 -2
- package/dist/ui/agent-chat-composer.d.ts +2 -1
- package/dist/ui/agent-chat-queue.d.ts +0 -2
- package/dist/ui/index.cjs +306 -260
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.js +308 -263
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/tool-sessions.d.ts +10 -2
- package/dist/ui/use-agent-chat-composer.d.ts +25 -26
- package/dist/ui/use-audio-recorder.d.ts +4 -2
- package/package.json +4 -3
- package/scaffold/host-styles.css +3 -0
- package/scaffold/manifest.json +2 -2
- package/scaffold/src/app.tsx +4 -1
- package/scaffold/src/chat.css +11 -1
- package/scaffold/src/components/chat/chat-composer.tsx +36 -19
- package/scaffold/src/components/chat/chat-shell.tsx +5 -4
- package/scaffold/src/components/chat/external-link-dialog.tsx +6 -5
- package/scaffold/src/components/chat/identity-control.tsx +0 -1
- package/scaffold/src/components/chat/subagent-tasks.tsx +109 -0
- package/scaffold/src/components/theme-provider.tsx +30 -27
- package/scaffold/src/components/ui/tooltip.tsx +3 -0
- package/scaffold/src/host.css +11 -0
- package/scaffold/src/index.css +1 -213
- package/scaffold/src/index.tsx +4 -2
- package/scaffold/src/theme.css +215 -0
|
@@ -23,7 +23,13 @@ export type AgentChatToolSlotContext = {
|
|
|
23
23
|
resourceTargets: readonly ChatComputerResourceTarget[];
|
|
24
24
|
openResource: (target: ChatComputerResourceTarget) => void;
|
|
25
25
|
};
|
|
26
|
+
export type AgentChatActivityIconContext = {
|
|
27
|
+
label: string;
|
|
28
|
+
kind: "tool" | "group" | "planning" | "node";
|
|
29
|
+
part?: ChatToolPart | ChatNodeActivityPart;
|
|
30
|
+
};
|
|
26
31
|
export type AgentChatToolSlots = {
|
|
32
|
+
renderActivityIcon?: (context: AgentChatActivityIconContext) => React.ReactNode;
|
|
27
33
|
onExpandedChange?: (open: boolean, part?: ChatToolPart) => void;
|
|
28
34
|
onToolSelect?: (part: ChatToolPart) => void;
|
|
29
35
|
onResourceAction?: (target: ChatComputerResourceTarget, part: ChatToolPart) => void;
|
|
@@ -37,15 +43,17 @@ export declare function isAgentChatSubagent(part: ChatToolPart): boolean;
|
|
|
37
43
|
export declare function isAgentChatToolRunning(part: ChatToolPart): boolean;
|
|
38
44
|
export declare function latestRunningAgentChatToolId(parts: readonly ChatToolPart[]): string | null;
|
|
39
45
|
export declare function partitionAgentChatToolTimeline(parts: readonly ChatPart[], toolViewMode?: AgentChatToolViewMode): AgentChatToolTimelineSegment[];
|
|
40
|
-
export declare function AgentChatActivityRow({ label, modelTurnId, onSelect, }: {
|
|
46
|
+
export declare function AgentChatActivityRow({ label, modelTurnId, renderActivityIcon, onSelect, }: {
|
|
41
47
|
label: string;
|
|
42
48
|
modelTurnId?: string;
|
|
49
|
+
renderActivityIcon?: AgentChatToolSlots["renderActivityIcon"];
|
|
43
50
|
onSelect?: () => void;
|
|
44
51
|
}): import("react/jsx-runtime").JSX.Element;
|
|
45
52
|
export declare function AgentChatModelTurnRow({ part }: {
|
|
46
53
|
part: ChatModelTurnPart;
|
|
47
54
|
}): import("react/jsx-runtime").JSX.Element;
|
|
48
|
-
export declare function AgentChatNodeActivityRow({ onSelect, part, }: {
|
|
55
|
+
export declare function AgentChatNodeActivityRow({ renderActivityIcon, onSelect, part, }: {
|
|
56
|
+
renderActivityIcon?: AgentChatToolSlots["renderActivityIcon"];
|
|
49
57
|
onSelect?: (part: ChatNodeActivityPart) => void;
|
|
50
58
|
part: ChatNodeActivityPart;
|
|
51
59
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { AgentChatInputContract } from "../runtime/input-contract";
|
|
3
3
|
import { type AgentChatComposerFile } from "./composer-attachments";
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
export type UseAgentChatComposerOptions = {
|
|
4
|
+
export type AgentChatComposerOptions = {
|
|
5
|
+
inputContract?: AgentChatInputContract | null;
|
|
6
|
+
recordingEnabled?: boolean;
|
|
7
|
+
textareaRef?: React.RefObject<HTMLTextAreaElement | null>;
|
|
10
8
|
disabled?: boolean;
|
|
11
9
|
isRunning?: boolean;
|
|
12
10
|
isCancelling?: boolean;
|
|
13
|
-
|
|
14
|
-
recordingEnabled?: boolean;
|
|
11
|
+
forceExpanded?: boolean;
|
|
15
12
|
focusKey?: string | null;
|
|
16
|
-
restoredDraft?:
|
|
17
|
-
|
|
18
|
-
onSubmit: (input: {
|
|
13
|
+
restoredDraft?: {
|
|
14
|
+
revision: number;
|
|
19
15
|
text: string;
|
|
20
16
|
files: AgentChatComposerFile[];
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
} | null;
|
|
18
|
+
onSubmit(text: string, files: AgentChatComposerFile[]): void | Promise<void>;
|
|
19
|
+
/** Observe the shared recorder. Do not stop its tracks or start another capture. */
|
|
20
|
+
onRecordingStreamChange?: (stream: MediaStream | null) => void;
|
|
23
21
|
};
|
|
24
|
-
|
|
25
|
-
export declare function useAgentChatComposer({ disabled, isRunning, isCancelling, inputContract, recordingEnabled, focusKey, restoredDraft, textareaRef, onSubmit }: UseAgentChatComposerOptions): {
|
|
22
|
+
export declare function useAgentChatComposer(options: AgentChatComposerOptions): {
|
|
26
23
|
text: string;
|
|
24
|
+
setText: (value: string) => void;
|
|
27
25
|
files: AgentChatComposerFile[];
|
|
28
|
-
|
|
26
|
+
addFiles: (selected: readonly File[]) => void;
|
|
27
|
+
removeFile: (id: string) => void;
|
|
28
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
29
|
+
submit: () => Promise<void>;
|
|
29
30
|
canSubmit: boolean;
|
|
30
|
-
|
|
31
|
+
isSubmitting: boolean;
|
|
31
32
|
attachmentError: string | null;
|
|
32
|
-
|
|
33
|
+
attachmentMimeTypes: string[];
|
|
34
|
+
validationError: string | null;
|
|
33
35
|
recorder: {
|
|
34
36
|
error: string | null;
|
|
35
37
|
isRecording: boolean;
|
|
36
38
|
start: () => Promise<void>;
|
|
37
39
|
stop: () => void;
|
|
38
40
|
};
|
|
39
|
-
|
|
40
|
-
fileAccept: string
|
|
41
|
+
recordingAllowed: boolean;
|
|
42
|
+
fileAccept: string;
|
|
43
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
41
44
|
allowAttachments: boolean;
|
|
45
|
+
isExpanded: boolean;
|
|
42
46
|
action: "stop" | "send";
|
|
43
|
-
setText: (value: string) => void;
|
|
44
|
-
addFiles: (selected: readonly File[]) => void;
|
|
45
|
-
removeFile: (id: string) => void;
|
|
46
|
-
submit: () => Promise<void>;
|
|
47
|
-
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
48
47
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type AudioRecorderOptions = {
|
|
2
|
+
/** Subscribe to the recorder-owned stream; consumers must not stop its tracks. */
|
|
2
3
|
onStreamChange?: (stream: MediaStream | null) => void;
|
|
3
|
-
}
|
|
4
|
+
};
|
|
5
|
+
export declare function useAudioRecorder(onRecorded: (file: File) => void, options?: AudioRecorderOptions): {
|
|
4
6
|
error: string | null;
|
|
5
7
|
isRecording: boolean;
|
|
6
8
|
start: () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents24/chat-react",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.31",
|
|
4
4
|
"description": "Accessible web UI and complete default layout for Agents24 chat applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"./scaffold/styles.css": "./scaffold/styles.css",
|
|
38
38
|
"./styles.css": "./dist/styles.css",
|
|
39
39
|
"./compatibility.json": "./compatibility.json",
|
|
40
|
+
"./scaffold/host-styles.css": "./scaffold/host-styles.css",
|
|
40
41
|
"./scaffold/inherit.css": "./scaffold/inherit.css",
|
|
41
42
|
"./registry/agents24-chat.json": "./dist/registry/agents24-chat.json"
|
|
42
43
|
},
|
|
@@ -63,8 +64,8 @@
|
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"@untitledui/file-icons": "0.0.9",
|
|
66
|
-
"@agents24/client": "0.2.
|
|
67
|
-
"@agents24/react": "0.2.
|
|
67
|
+
"@agents24/client": "0.2.15",
|
|
68
|
+
"@agents24/react": "0.2.19",
|
|
68
69
|
"@fontsource-variable/inter": "^5.3.0",
|
|
69
70
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
70
71
|
"@fontsource-variable/merriweather": "^5.3.0",
|
package/scaffold/manifest.json
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"@shadcn/react": "^0.2.1",
|
|
15
15
|
"class-variance-authority": "^0.7.1",
|
|
16
16
|
"clsx": "^2.1.1",
|
|
17
|
-
"lucide-react": "0.542.0",
|
|
18
|
-
"radix-ui": "1.4.3",
|
|
17
|
+
"lucide-react": ">=0.542.0 <2.0.0",
|
|
18
|
+
"radix-ui": ">=1.4.3 <2.0.0",
|
|
19
19
|
"streamdown": "^2.5.0",
|
|
20
20
|
"tailwind-merge": "^3.6.0",
|
|
21
21
|
"tw-animate-css": "^1.4.0"
|
package/scaffold/src/app.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import * as React from "react"
|
|
17
17
|
|
|
18
18
|
import type { Agents24ChatComposerProps } from "./components/chat/chat-composer"
|
|
19
|
+
|
|
19
20
|
import { ChatShell } from "./components/chat/chat-shell"
|
|
20
21
|
import { GUEST_CHAT_IDENTITY, type Agents24ChatIdentity } from "./identity"
|
|
21
22
|
|
|
@@ -36,9 +37,11 @@ export type Agents24ChatAppProps = {
|
|
|
36
37
|
sidebar?: React.ReactNode
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export type Agents24ChatComposerSlotProps = Agents24ChatComposerProps
|
|
41
|
+
|
|
39
42
|
export type Agents24ChatScaffoldComponents = {
|
|
43
|
+
Composer?: React.ComponentType<Agents24ChatComposerSlotProps>
|
|
40
44
|
Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
|
|
41
|
-
Composer?: React.ComponentType<Agents24ChatComposerProps>
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export type ChatHitlResponse = HitlResponse
|
package/scaffold/src/chat.css
CHANGED
|
@@ -50,10 +50,20 @@
|
|
|
50
50
|
grid-template-areas: "textarea textarea textarea" "attach spacer submit";
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
[data-agents24-theme-root] .agents24-composer-grid:not([data-expanded]) {
|
|
53
|
+
[data-agents24-theme-root] .agents24-composer-grid:not([data-expanded]):not([data-attachments]) {
|
|
54
54
|
border-radius: var(--a24-composer-single-row-radius);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/* Draft attachments stay within the same surface and precede its input rows. */
|
|
58
|
+
.agents24-composer-grid[data-attachments] {
|
|
59
|
+
grid-template-areas: "attachments attachments attachments" "attach textarea submit";
|
|
60
|
+
border-radius: 1.125rem;
|
|
61
|
+
}
|
|
62
|
+
.agents24-composer-grid[data-attachments][data-expanded] {
|
|
63
|
+
grid-template-areas: "attachments attachments attachments" "textarea textarea textarea" "attach spacer submit";
|
|
64
|
+
}
|
|
65
|
+
.agents24-composer-attachments { grid-area: attachments; }
|
|
66
|
+
|
|
57
67
|
.agents24-composer-attach { grid-area: attach; }
|
|
58
68
|
.agents24-composer-textarea { grid-area: textarea; }
|
|
59
69
|
.agents24-composer-actions { grid-area: submit; }
|
|
@@ -20,6 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
|
|
|
20
20
|
import { cn } from "../../lib/utils"
|
|
21
21
|
|
|
22
22
|
export type Agents24ChatComposerProps = {
|
|
23
|
+
queue?: React.ReactNode
|
|
24
|
+
onRecordingStreamChange?: (stream: MediaStream | null) => void
|
|
23
25
|
inputContract: AgentChatInputContract
|
|
24
26
|
disabled?: boolean
|
|
25
27
|
contextWindow?: ContextWindow | null
|
|
@@ -41,6 +43,8 @@ export type Agents24ChatComposerProps = {
|
|
|
41
43
|
|
|
42
44
|
export function ChatComposer({
|
|
43
45
|
inputContract,
|
|
46
|
+
queue,
|
|
47
|
+
onRecordingStreamChange,
|
|
44
48
|
disabled,
|
|
45
49
|
contextWindow,
|
|
46
50
|
focusKey,
|
|
@@ -59,12 +63,12 @@ export function ChatComposer({
|
|
|
59
63
|
utilityRow = "inline",
|
|
60
64
|
}: Agents24ChatComposerProps) {
|
|
61
65
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
62
|
-
const textareaRef
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const { text, setText, files, removeFile, addFiles, textareaRef, submit,
|
|
67
|
+
attachmentError, attachmentMimeTypes, allowAttachments, recordingAllowed,
|
|
68
|
+
recorder, isExpanded, validationError, action, canSubmit } = useAgentChatComposer({
|
|
69
|
+
inputContract, disabled, focusKey, forceExpanded, isCancelling, isRunning,
|
|
70
|
+
onSubmit, restoredDraft, onRecordingStreamChange,
|
|
71
|
+
})
|
|
68
72
|
|
|
69
73
|
const selectedAgent = agents.find((item) => item.id === agentId)
|
|
70
74
|
const selectedModel = models.find((item) => item.id === modelId)
|
|
@@ -129,22 +133,28 @@ export function ChatComposer({
|
|
|
129
133
|
className="relative z-10 w-full pb-[max(1rem,env(safe-area-inset-bottom))]"
|
|
130
134
|
data-agents24-chat-composer=""
|
|
131
135
|
>
|
|
132
|
-
{
|
|
133
|
-
<div className="mb-2 min-w-0">
|
|
134
|
-
<ChatAttachmentRows
|
|
135
|
-
attachments={files}
|
|
136
|
-
onRemove={(attachment) => { if (attachment.id) composer.removeFile(attachment.id) }}
|
|
137
|
-
/>
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
136
|
+
{queue}
|
|
140
137
|
{attachmentError || recorder.error ? <p className="mb-2 text-xs text-destructive" role="alert">{attachmentError || recorder.error}</p> : null}
|
|
141
138
|
<div
|
|
142
139
|
data-expanded={isExpanded || undefined}
|
|
140
|
+
data-attachments={files.length > 0 || undefined}
|
|
143
141
|
className={cn(
|
|
144
142
|
"agents24-composer-grid grid min-w-0 items-center gap-2 rounded-xl border bg-background shadow-sm",
|
|
145
143
|
isExpanded ? "p-2" : "p-1.5",
|
|
146
144
|
)}
|
|
147
145
|
>
|
|
146
|
+
{allowAttachments && files.length > 0 && (
|
|
147
|
+
<div className="agents24-composer-attachments min-w-0">
|
|
148
|
+
<ChatAttachmentRows
|
|
149
|
+
layout="composer"
|
|
150
|
+
attachments={files}
|
|
151
|
+
onRemove={(attachment) => {
|
|
152
|
+
if (!attachment.id) return
|
|
153
|
+
removeFile(attachment.id)
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
148
158
|
{allowAttachments ? (
|
|
149
159
|
<>
|
|
150
160
|
<input
|
|
@@ -152,9 +162,9 @@ export function ChatComposer({
|
|
|
152
162
|
className="sr-only"
|
|
153
163
|
type="file"
|
|
154
164
|
multiple
|
|
155
|
-
accept={
|
|
165
|
+
accept={attachmentMimeTypes.join(",")}
|
|
156
166
|
onChange={(event) => {
|
|
157
|
-
|
|
167
|
+
addFiles(Array.from(event.target.files ?? []))
|
|
158
168
|
event.currentTarget.value = ""
|
|
159
169
|
}}
|
|
160
170
|
/>
|
|
@@ -176,8 +186,15 @@ export function ChatComposer({
|
|
|
176
186
|
"agents24-composer-textarea max-h-56 min-h-9 field-sizing-content resize-none overflow-y-auto border-0 bg-transparent px-2 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
177
187
|
isExpanded ? "py-2" : "h-9 py-2",
|
|
178
188
|
)}
|
|
179
|
-
onChange={(event) =>
|
|
180
|
-
|
|
189
|
+
onChange={(event) => {
|
|
190
|
+
setText(event.target.value)
|
|
191
|
+
}}
|
|
192
|
+
onKeyDown={(event) => {
|
|
193
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
194
|
+
event.preventDefault()
|
|
195
|
+
void submit()
|
|
196
|
+
}
|
|
197
|
+
}}
|
|
181
198
|
/>
|
|
182
199
|
<div className="agents24-composer-actions flex items-center gap-1">
|
|
183
200
|
{utilityRow === "inline" ? <AgentChatContextStatus contextWindow={contextWindow} /> : null}
|
|
@@ -208,7 +225,7 @@ export function ChatComposer({
|
|
|
208
225
|
<TooltipContent>{validationError}</TooltipContent>
|
|
209
226
|
</Tooltip>
|
|
210
227
|
) : (
|
|
211
|
-
<Button size="icon" aria-label={isRunning ? "Queue message" : "Send message"} disabled={
|
|
228
|
+
<Button size="icon" aria-label={isRunning ? "Queue message" : "Send message"} disabled={!canSubmit} onClick={() => void submit()}><ArrowUp /></Button>
|
|
212
229
|
)}
|
|
213
230
|
</div>
|
|
214
231
|
</div>
|
|
@@ -11,6 +11,7 @@ import type { Agents24ChatIdentity } from "../../identity"
|
|
|
11
11
|
|
|
12
12
|
import { ChatComposer } from "./chat-composer"
|
|
13
13
|
import { ChatMessage } from "./chat-message"
|
|
14
|
+
import { SubagentTasks } from "./subagent-tasks"
|
|
14
15
|
import { ThemeMenu } from "./theme-menu"
|
|
15
16
|
import { ThreadSidebar } from "./thread-sidebar"
|
|
16
17
|
import { TurnOutline } from "./turn-outline"
|
|
@@ -168,6 +169,7 @@ export function ChatShell({
|
|
|
168
169
|
data-agents24-chat-transcript=""
|
|
169
170
|
>
|
|
170
171
|
<OperationErrorNotice runtime={runtime} />
|
|
172
|
+
<SubagentTasks threadId={controller.activeThreadId} />
|
|
171
173
|
<LatestThreadScroller.Provider autoScroll defaultScrollPosition="last-anchor" scrollPreviousItemPeek={64}>
|
|
172
174
|
{!isEmptyState && (
|
|
173
175
|
<LatestThreadScroller.Root className="relative min-h-0 flex-1">
|
|
@@ -240,11 +242,10 @@ export function ChatShell({
|
|
|
240
242
|
<LatestThreadScroller.Button className="!relative !inset-auto !translate-x-0 rtl:!translate-x-0" />
|
|
241
243
|
</div>}
|
|
242
244
|
<div className="pointer-events-auto mx-auto w-full max-w-3xl px-4 sm:px-6">
|
|
243
|
-
<AgentChatQueue pending={controller.pendingMessages} queue={controller.messageQueue}
|
|
244
|
-
canSteer={controller.runState === "streaming"}
|
|
245
|
-
onSteer={controller.steerPendingMessage} onRemove={controller.removePendingMessage}
|
|
246
|
-
onEdit={retractForEdit} onResume={controller.resumeQueue} />
|
|
247
245
|
<Composer
|
|
246
|
+
queue={<AgentChatQueue pending={controller.pendingMessages} queue={controller.messageQueue}
|
|
247
|
+
onRemove={controller.removePendingMessage}
|
|
248
|
+
onEdit={retractForEdit} onResume={controller.resumeQueue} />}
|
|
248
249
|
restoredDraft={restoredDraft}
|
|
249
250
|
inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
|
|
250
251
|
contextWindow={runtime.contextWindow}
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
export function ExternalLinkDialog({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) {
|
|
15
15
|
const [copied, setCopied] = React.useState(false)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const close = () => {
|
|
18
|
+
setCopied(false)
|
|
19
|
+
onClose()
|
|
20
|
+
}
|
|
20
21
|
|
|
21
22
|
const copyUrl = async () => {
|
|
22
23
|
await navigator.clipboard.writeText(url)
|
|
@@ -25,11 +26,11 @@ export function ExternalLinkDialog({ isOpen, onClose, onConfirm, url }: LinkSafe
|
|
|
25
26
|
|
|
26
27
|
const openUrl = () => {
|
|
27
28
|
onConfirm()
|
|
28
|
-
|
|
29
|
+
close()
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
|
-
<Dialog open={isOpen} onOpenChange={(open) => { if (!open)
|
|
33
|
+
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) close() }}>
|
|
33
34
|
<DialogContent className="max-h-[calc(100dvh-2rem)] min-w-0 overflow-hidden sm:max-w-md">
|
|
34
35
|
<DialogHeader>
|
|
35
36
|
<DialogTitle>Open external link?</DialogTitle>
|
|
@@ -21,7 +21,6 @@ function identityLabel(identity: Agents24ChatIdentity): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function IdentityAvatar({ identity }: { identity: Agents24ChatIdentity }) {
|
|
24
|
-
const label = identityLabel(identity)
|
|
25
24
|
if (identity.avatarUrl) {
|
|
26
25
|
return <img alt="" className="size-6 rounded-full object-cover" src={identity.avatarUrl} />
|
|
27
26
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { useAgentClient, partsFromResponseBlocks } from "@agents24/react"
|
|
3
|
+
import { AgentResponseTimeline } from "@agents24/chat-react/ui"
|
|
4
|
+
import { useDelegationTasks, type DelegationHistory, type DelegationTask } from "@agents24/chat-react/runtime"
|
|
5
|
+
import { Button } from "../ui/button"
|
|
6
|
+
|
|
7
|
+
export function SubagentTasks({ threadId }: { threadId: string | null }) {
|
|
8
|
+
const client = useAgentClient()
|
|
9
|
+
const source = React.useMemo(() => ({ events: (input: { threadId: string; after: number; signal: AbortSignal }) =>
|
|
10
|
+
client.threads.delegations.events(input) }), [client])
|
|
11
|
+
const { tasks, error, refresh } = useDelegationTasks({ threadId, source })
|
|
12
|
+
const [selection, setSelection] = React.useState<{ threadId: string | null; handle: string | null }>({ threadId: null, handle: null })
|
|
13
|
+
const selected = selection.threadId === threadId ? selection.handle : null
|
|
14
|
+
if (!threadId || (!tasks.length && !error)) return null
|
|
15
|
+
return <section aria-label="Subagent tasks" className="mx-auto w-full max-w-3xl px-4 py-2 text-sm">
|
|
16
|
+
<details>
|
|
17
|
+
<summary className="cursor-pointer font-medium">Tasks ({tasks.length})</summary>
|
|
18
|
+
{error ? <p role="alert">Task updates are unavailable. Retrying…</p> : null}
|
|
19
|
+
<ul className="max-h-56 overflow-y-auto">
|
|
20
|
+
{tasks.map(task => <li key={task.handle} style={{ paddingInlineStart: Math.max(0, task.depth - 1) * 12 }}>
|
|
21
|
+
<button type="button" className="flex w-full items-center justify-between gap-3 rounded-md px-2 py-2 text-start hover:bg-muted"
|
|
22
|
+
onClick={() => setSelection({ threadId, handle: selected === task.handle ? null : task.handle })} aria-expanded={selected === task.handle}>
|
|
23
|
+
<span className="min-w-0 truncate">{task.name}{task.background ? " · Background" : ""}</span>
|
|
24
|
+
<span className="shrink-0 text-xs text-muted-foreground">{task.status.replaceAll("_", " ")}</span>
|
|
25
|
+
</button>
|
|
26
|
+
</li>)}
|
|
27
|
+
</ul>
|
|
28
|
+
{tasks.filter(task => task.handle === selected).map(task =>
|
|
29
|
+
<TaskConversation key={`${threadId}:${task.handle}`} threadId={threadId} task={task} onChanged={refresh} />)}
|
|
30
|
+
</details>
|
|
31
|
+
</section>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function TaskConversation({ threadId, task, onChanged }: {
|
|
35
|
+
threadId: string; task: DelegationTask; onChanged: () => void
|
|
36
|
+
}) {
|
|
37
|
+
const client = useAgentClient()
|
|
38
|
+
const [history, setHistory] = React.useState<DelegationHistory | null>(null)
|
|
39
|
+
const [earlier, setEarlier] = React.useState<DelegationHistory["turns"]>([])
|
|
40
|
+
const historyInitialized = React.useRef(false)
|
|
41
|
+
const [before, setBefore] = React.useState<number | null>(null)
|
|
42
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
43
|
+
const [busy, setBusy] = React.useState(false)
|
|
44
|
+
const operation = React.useRef<{ action: "interrupt" | "cancel"; id: string } | null>(null)
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const controller = new AbortController()
|
|
47
|
+
let timer: ReturnType<typeof setTimeout>
|
|
48
|
+
const refresh = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const page = await client.threads.delegations.history({ threadId, handle: task.handle, signal: controller.signal })
|
|
51
|
+
if (controller.signal.aborted) return
|
|
52
|
+
setHistory(page)
|
|
53
|
+
if (!historyInitialized.current) {
|
|
54
|
+
historyInitialized.current = true
|
|
55
|
+
setBefore(page.paging.next_before_turn_index)
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
if (!controller.signal.aborted) setError("Task history could not be loaded.")
|
|
59
|
+
} finally {
|
|
60
|
+
if (!controller.signal.aborted) timer = setTimeout(refresh, 2000)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
void refresh()
|
|
64
|
+
return () => { controller.abort(); clearTimeout(timer) }
|
|
65
|
+
}, [client, threadId, task.handle])
|
|
66
|
+
const loadEarlier = async () => {
|
|
67
|
+
if (before === null) return
|
|
68
|
+
setBusy(true)
|
|
69
|
+
try {
|
|
70
|
+
const page = await client.threads.delegations.history({ threadId, handle: task.handle, before })
|
|
71
|
+
setEarlier(current => [...page.turns, ...current])
|
|
72
|
+
setBefore(page.paging.next_before_turn_index)
|
|
73
|
+
} catch { setError("Earlier task history could not be loaded.") }
|
|
74
|
+
finally { setBusy(false) }
|
|
75
|
+
}
|
|
76
|
+
const act = async (action: "interrupt" | "cancel") => {
|
|
77
|
+
setBusy(true); setError(null)
|
|
78
|
+
if (operation.current?.action !== action) operation.current = { action, id: crypto.randomUUID() }
|
|
79
|
+
try {
|
|
80
|
+
await client.threads.delegations.act({ threadId, handle: task.handle,
|
|
81
|
+
action: { action, operation_id: operation.current.id } })
|
|
82
|
+
operation.current = null
|
|
83
|
+
onChanged()
|
|
84
|
+
} catch { setError("The task action could not be completed. Try again.") }
|
|
85
|
+
finally { setBusy(false) }
|
|
86
|
+
}
|
|
87
|
+
const turns = Array.from(new Map([...earlier, ...(history?.turns || [])].map(turn => [turn.id, turn])).values())
|
|
88
|
+
.sort((a, b) => a.turn_index - b.turn_index)
|
|
89
|
+
return <div className="space-y-3 rounded-md bg-muted/30 p-3">
|
|
90
|
+
<h3 className="font-medium">{task.name}</h3>
|
|
91
|
+
{error ? <p role="alert">{error}</p> : null}
|
|
92
|
+
<div className="flex flex-wrap gap-2">
|
|
93
|
+
{task.capabilities.interrupt ? <Button variant="secondary" size="sm" disabled={busy} onClick={() => void act("interrupt")}>Interrupt</Button> : null}
|
|
94
|
+
{task.capabilities.cancel ? <Button variant="secondary" size="sm" disabled={busy} onClick={() => void act("cancel")}>Cancel task</Button> : null}
|
|
95
|
+
{before !== null ? <Button variant="ghost" size="sm" disabled={busy} onClick={() => void loadEarlier()}>Earlier exchanges</Button> : null}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="max-h-[55dvh] space-y-4 overflow-y-auto break-words">
|
|
98
|
+
{turns.map(turn => <article key={turn.id} className="space-y-2">
|
|
99
|
+
{turn.assignment_instruction ? <p className="whitespace-pre-wrap text-muted-foreground">{turn.assignment_instruction}</p> : null}
|
|
100
|
+
{turn.user_input_text ? <p className="whitespace-pre-wrap">{turn.user_input_text}</p> : null}
|
|
101
|
+
{turn.exchanges?.map(exchange => <div key={exchange.id} className="whitespace-pre-wrap break-words text-sm">
|
|
102
|
+
<p className="text-xs font-medium">{exchange.sender === "parent" ? "Parent Agent" : "Subagent update"}</p>
|
|
103
|
+
{exchange.text}
|
|
104
|
+
</div>)}
|
|
105
|
+
<AgentResponseTimeline message={{ id: turn.id, role: "assistant", createdAt: turn.created_at, content: turn.assistant_output_text || "", parts: partsFromResponseBlocks(turn.response_blocks) }} />
|
|
106
|
+
</article>)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
|
|
3
3
|
export type Agents24ChatAppearance = "system" | "light" | "dark"
|
|
4
|
-
export type Agents24ChatTheme = "soft" | "sharp"
|
|
4
|
+
export type Agents24ChatTheme = "soft" | "sharp"
|
|
5
5
|
|
|
6
6
|
export type Agents24ChatAppearanceController = {
|
|
7
7
|
appearance: Agents24ChatAppearance
|
|
@@ -9,13 +9,28 @@ export type Agents24ChatAppearanceController = {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
type ThemeContextValue = {
|
|
12
|
-
appearance: Agents24ChatAppearance
|
|
13
12
|
inherited: boolean
|
|
13
|
+
appearance: Agents24ChatAppearance
|
|
14
14
|
portalContainer: HTMLElement | null
|
|
15
15
|
setAppearance: (appearance: Agents24ChatAppearance) => void
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const ThemeContext = React.createContext<ThemeContextValue | null>(null)
|
|
19
|
+
const appearanceEvent = "agents24-chat-appearance-change"
|
|
20
|
+
const subscribeAppearance = (notify: () => void) => {
|
|
21
|
+
window.addEventListener("storage", notify)
|
|
22
|
+
window.addEventListener(appearanceEvent, notify)
|
|
23
|
+
return () => {
|
|
24
|
+
window.removeEventListener("storage", notify)
|
|
25
|
+
window.removeEventListener(appearanceEvent, notify)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const subscribeDarkMode = (notify: () => void) => {
|
|
29
|
+
const media = window.matchMedia?.("(prefers-color-scheme: dark)")
|
|
30
|
+
media?.addEventListener("change", notify)
|
|
31
|
+
return () => media?.removeEventListener("change", notify)
|
|
32
|
+
}
|
|
33
|
+
const getDarkMode = () => window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false
|
|
19
34
|
const storageKey = "agents24-chat-appearance"
|
|
20
35
|
|
|
21
36
|
function storedAppearance(): Agents24ChatAppearance {
|
|
@@ -31,48 +46,36 @@ export function ThemeProvider({
|
|
|
31
46
|
children,
|
|
32
47
|
controller,
|
|
33
48
|
theme = "soft",
|
|
49
|
+
mode = "isolated",
|
|
34
50
|
}: {
|
|
35
51
|
children: React.ReactNode
|
|
36
52
|
controller?: Agents24ChatAppearanceController
|
|
37
53
|
theme?: Agents24ChatTheme
|
|
54
|
+
mode?: "isolated" | "inherit"
|
|
38
55
|
}) {
|
|
39
|
-
const [
|
|
40
|
-
const
|
|
41
|
-
const
|
|
56
|
+
const [sessionAppearance, setSessionAppearance] = React.useState<Agents24ChatAppearance | null>(null)
|
|
57
|
+
const stored = React.useSyncExternalStore(subscribeAppearance, storedAppearance, () => "system" as const)
|
|
58
|
+
const systemDark = React.useSyncExternalStore(subscribeDarkMode, getDarkMode, () => false)
|
|
42
59
|
const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null)
|
|
43
|
-
const appearance = controller?.appearance ??
|
|
44
|
-
|
|
45
|
-
React.useEffect(() => {
|
|
46
|
-
if (theme !== "inherit" && !controller) setInternalAppearance(storedAppearance())
|
|
47
|
-
setMounted(true)
|
|
48
|
-
}, [controller, theme])
|
|
49
|
-
|
|
50
|
-
React.useEffect(() => {
|
|
51
|
-
if (theme === "inherit" || !mounted || typeof window === "undefined" || typeof window.matchMedia !== "function") return
|
|
52
|
-
const media = window.matchMedia("(prefers-color-scheme: dark)")
|
|
53
|
-
const apply = () => setResolvedAppearance(
|
|
54
|
-
appearance === "system" ? media.matches ? "dark" : "light" : appearance,
|
|
55
|
-
)
|
|
56
|
-
apply()
|
|
57
|
-
media.addEventListener("change", apply)
|
|
58
|
-
return () => media.removeEventListener("change", apply)
|
|
59
|
-
}, [appearance, mounted, theme])
|
|
60
|
+
const appearance = controller?.appearance ?? sessionAppearance ?? stored
|
|
61
|
+
const resolvedAppearance = appearance === "system" ? (systemDark ? "dark" : "light") : appearance
|
|
60
62
|
|
|
61
63
|
const change = React.useCallback((next: Agents24ChatAppearance) => {
|
|
62
|
-
if (theme === "inherit") return
|
|
63
64
|
if (controller) {
|
|
64
65
|
controller.setAppearance(next)
|
|
65
66
|
return
|
|
66
67
|
}
|
|
68
|
+
setSessionAppearance(next)
|
|
67
69
|
try { window.localStorage.setItem(storageKey, next) } catch { /* storage is optional */ }
|
|
68
|
-
|
|
69
|
-
}, [controller
|
|
70
|
+
window.dispatchEvent(new Event(appearanceEvent))
|
|
71
|
+
}, [controller])
|
|
70
72
|
|
|
71
73
|
return (
|
|
72
|
-
<ThemeContext.Provider value={{ appearance, inherited:
|
|
74
|
+
<ThemeContext.Provider value={{ appearance, portalContainer, setAppearance: change, inherited: mode === "inherit" }}>
|
|
73
75
|
<div
|
|
74
76
|
ref={setPortalContainer}
|
|
75
|
-
className={
|
|
77
|
+
className={mode === "inherit" ? "size-full" : `${resolvedAppearance} size-full`}
|
|
78
|
+
data-agents24-theme-mode={mode}
|
|
76
79
|
data-agents24-theme={theme}
|
|
77
80
|
data-agents24-theme-root=""
|
|
78
81
|
>
|
|
@@ -21,6 +21,7 @@ function TooltipTrigger({
|
|
|
21
21
|
asChild: _asChild,
|
|
22
22
|
...props
|
|
23
23
|
}: React.ComponentProps<typeof Slot.Root> & { asChild?: boolean }) {
|
|
24
|
+
void _asChild
|
|
24
25
|
return <Slot.Root data-slot="tooltip-trigger" {...props} />
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -35,6 +36,8 @@ function TooltipContent({
|
|
|
35
36
|
sideOffset?: number
|
|
36
37
|
align?: "start" | "center" | "end"
|
|
37
38
|
}) {
|
|
39
|
+
void _align
|
|
40
|
+
void _sideOffset
|
|
38
41
|
return (
|
|
39
42
|
<span
|
|
40
43
|
role="tooltip"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@layer theme, base, components, utilities;
|
|
2
|
+
@import "tailwindcss/theme.css" layer(theme);
|
|
3
|
+
@import "tailwindcss/utilities.css" layer(utilities);
|
|
4
|
+
@import "tw-animate-css";
|
|
5
|
+
@import "@fontsource-variable/jetbrains-mono";
|
|
6
|
+
@import "@fontsource-variable/merriweather";
|
|
7
|
+
@import "@fontsource-variable/inter";
|
|
8
|
+
|
|
9
|
+
@source "./**/*.{ts,tsx}";
|
|
10
|
+
|
|
11
|
+
@import "./theme.css";
|