@agents24/chat-react 0.5.29 → 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 +28 -1
- package/dist/chunk-INO5DSSJ.js +86 -0
- package/dist/chunk-INO5DSSJ.js.map +1 -0
- package/dist/generated/delegation-types.d.ts +129 -0
- package/dist/registry/agents24-chat.json +267 -0
- package/dist/runtime/index.cjs +53 -1
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +62 -78
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/input-contract.d.ts +3 -1
- package/dist/runtime/use-delegation-tasks.d.ts +20 -0
- package/dist/scaffold/app.d.ts +3 -0
- package/dist/scaffold/components/chat/chat-composer.d.ts +5 -3
- package/dist/scaffold/components/chat/subagent-tasks.d.ts +3 -0
- package/dist/scaffold/components/chat/theme-menu.d.ts +1 -1
- package/dist/scaffold/components/theme-provider.d.ts +3 -1
- package/dist/scaffold/index.cjs +420 -333
- package/dist/scaffold/index.cjs.map +1 -1
- package/dist/scaffold/index.d.ts +5 -2
- package/dist/scaffold/index.js +415 -332
- 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 +8 -2
- package/dist/ui/agent-chat-queue.d.ts +0 -2
- package/dist/ui/index.cjs +817 -623
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +776 -622
- 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 +47 -0
- package/dist/ui/use-audio-recorder.d.ts +5 -3
- package/package.json +11 -6
- package/scaffold/host-styles.css +3 -0
- package/scaffold/inherit.css +4 -0
- package/scaffold/manifest.json +2 -2
- package/scaffold/src/app.tsx +5 -0
- package/scaffold/src/chat.css +35 -1
- package/scaffold/src/components/chat/chat-composer.tsx +31 -97
- package/scaffold/src/components/chat/chat-shell.tsx +7 -5
- 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/chat/theme-menu.tsx +3 -1
- package/scaffold/src/components/theme-provider.tsx +28 -23
- 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 +9 -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;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { AgentChatInputContract } from "../runtime/input-contract";
|
|
3
|
+
import { type AgentChatComposerFile } from "./composer-attachments";
|
|
4
|
+
export type AgentChatComposerOptions = {
|
|
5
|
+
inputContract?: AgentChatInputContract | null;
|
|
6
|
+
recordingEnabled?: boolean;
|
|
7
|
+
textareaRef?: React.RefObject<HTMLTextAreaElement | null>;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isRunning?: boolean;
|
|
10
|
+
isCancelling?: boolean;
|
|
11
|
+
forceExpanded?: boolean;
|
|
12
|
+
focusKey?: string | null;
|
|
13
|
+
restoredDraft?: {
|
|
14
|
+
revision: number;
|
|
15
|
+
text: string;
|
|
16
|
+
files: AgentChatComposerFile[];
|
|
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;
|
|
21
|
+
};
|
|
22
|
+
export declare function useAgentChatComposer(options: AgentChatComposerOptions): {
|
|
23
|
+
text: string;
|
|
24
|
+
setText: (value: string) => void;
|
|
25
|
+
files: AgentChatComposerFile[];
|
|
26
|
+
addFiles: (selected: readonly File[]) => void;
|
|
27
|
+
removeFile: (id: string) => void;
|
|
28
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
29
|
+
submit: () => Promise<void>;
|
|
30
|
+
canSubmit: boolean;
|
|
31
|
+
isSubmitting: boolean;
|
|
32
|
+
attachmentError: string | null;
|
|
33
|
+
attachmentMimeTypes: string[];
|
|
34
|
+
validationError: string | null;
|
|
35
|
+
recorder: {
|
|
36
|
+
error: string | null;
|
|
37
|
+
isRecording: boolean;
|
|
38
|
+
start: () => Promise<void>;
|
|
39
|
+
stop: () => void;
|
|
40
|
+
};
|
|
41
|
+
recordingAllowed: boolean;
|
|
42
|
+
fileAccept: string;
|
|
43
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
44
|
+
allowAttachments: boolean;
|
|
45
|
+
isExpanded: boolean;
|
|
46
|
+
action: "stop" | "send";
|
|
47
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
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>;
|
|
7
|
-
stop: () => void
|
|
9
|
+
stop: () => void;
|
|
8
10
|
};
|
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": {
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
},
|
|
37
37
|
"./scaffold/styles.css": "./scaffold/styles.css",
|
|
38
38
|
"./styles.css": "./dist/styles.css",
|
|
39
|
-
"./compatibility.json": "./compatibility.json"
|
|
39
|
+
"./compatibility.json": "./compatibility.json",
|
|
40
|
+
"./scaffold/host-styles.css": "./scaffold/host-styles.css",
|
|
41
|
+
"./scaffold/inherit.css": "./scaffold/inherit.css",
|
|
42
|
+
"./registry/agents24-chat.json": "./dist/registry/agents24-chat.json"
|
|
40
43
|
},
|
|
41
44
|
"files": [
|
|
42
45
|
"dist",
|
|
@@ -52,15 +55,17 @@
|
|
|
52
55
|
"scripts": {
|
|
53
56
|
"sync:contract": "node scripts/sync-contract.mjs",
|
|
54
57
|
"check:contract": "node scripts/sync-contract.mjs --check",
|
|
55
|
-
"build": "pnpm run sync:contract && tsup && tsc -p tsconfig.build.json && tsc -p tsconfig.scaffold-build.json",
|
|
58
|
+
"build": "pnpm run sync:contract && tsup && tsc -p tsconfig.build.json && tsc -p tsconfig.scaffold-build.json && pnpm run build:registry",
|
|
56
59
|
"test": "tsx --test tests/*.test.ts",
|
|
57
60
|
"check:boundaries": "node tests/check-boundaries.mjs",
|
|
58
|
-
"prepack": "pnpm run build && pnpm run check:boundaries"
|
|
61
|
+
"prepack": "pnpm run build && pnpm run check:boundaries",
|
|
62
|
+
"build:registry": "node scripts/generate-registry.mjs",
|
|
63
|
+
"check:registry": "node scripts/generate-registry.mjs --check"
|
|
59
64
|
},
|
|
60
65
|
"dependencies": {
|
|
61
66
|
"@untitledui/file-icons": "0.0.9",
|
|
62
|
-
"@agents24/client": "0.2.
|
|
63
|
-
"@agents24/react": "0.2.
|
|
67
|
+
"@agents24/client": "0.2.15",
|
|
68
|
+
"@agents24/react": "0.2.19",
|
|
64
69
|
"@fontsource-variable/inter": "^5.3.0",
|
|
65
70
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
66
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
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
} from "@agents24/react"
|
|
16
16
|
import * as React from "react"
|
|
17
17
|
|
|
18
|
+
import type { Agents24ChatComposerProps } from "./components/chat/chat-composer"
|
|
19
|
+
|
|
18
20
|
import { ChatShell } from "./components/chat/chat-shell"
|
|
19
21
|
import { GUEST_CHAT_IDENTITY, type Agents24ChatIdentity } from "./identity"
|
|
20
22
|
|
|
@@ -35,7 +37,10 @@ export type Agents24ChatAppProps = {
|
|
|
35
37
|
sidebar?: React.ReactNode
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export type Agents24ChatComposerSlotProps = Agents24ChatComposerProps
|
|
41
|
+
|
|
38
42
|
export type Agents24ChatScaffoldComponents = {
|
|
43
|
+
Composer?: React.ComponentType<Agents24ChatComposerSlotProps>
|
|
39
44
|
Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
|
|
40
45
|
}
|
|
41
46
|
|
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; }
|
|
@@ -64,3 +74,27 @@
|
|
|
64
74
|
display: -webkit-box;
|
|
65
75
|
overflow: hidden;
|
|
66
76
|
}
|
|
77
|
+
|
|
78
|
+
/* Host theme mode changes no document tokens or appearance. */
|
|
79
|
+
[data-agents24-theme-root][data-agents24-theme="inherit"] {
|
|
80
|
+
--a24-composer-single-row-radius: var(--radius-xl, var(--radius, .75rem));
|
|
81
|
+
font-family: inherit;
|
|
82
|
+
}
|
|
83
|
+
[data-agents24-theme-root][data-agents24-theme="inherit"] :focus-visible {
|
|
84
|
+
outline: 2px solid var(--ring, currentColor) !important;
|
|
85
|
+
outline-offset: 2px;
|
|
86
|
+
}
|
|
87
|
+
[data-agents24-theme-root][data-agents24-theme="inherit"] .a24-context-status__popover {
|
|
88
|
+
background: var(--popover, var(--background));
|
|
89
|
+
color: var(--popover-foreground, var(--foreground));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[data-agents24-theme-root][data-agents24-theme="inherit"] [data-slot="button"] {
|
|
93
|
+
border-radius: var(--radius);
|
|
94
|
+
}
|
|
95
|
+
[data-agents24-theme-root][data-agents24-theme="inherit"] .a24-context-status__popover {
|
|
96
|
+
border-color: var(--border);
|
|
97
|
+
}
|
|
98
|
+
[data-agents24-theme-root][data-agents24-theme="inherit"] .a24-context-status__popover > span:not(.a24-context-status__sr-only) {
|
|
99
|
+
color: var(--muted-foreground);
|
|
100
|
+
}
|
|
@@ -3,14 +3,10 @@ import * as React from "react"
|
|
|
3
3
|
import {
|
|
4
4
|
AgentChatContextStatus,
|
|
5
5
|
ChatAttachmentRows,
|
|
6
|
-
|
|
7
|
-
createAgentChatComposerFile,
|
|
8
|
-
revokeAgentChatComposerFiles,
|
|
9
|
-
useAudioRecorder,
|
|
10
|
-
useChatComposerFocus,
|
|
6
|
+
useAgentChatComposer,
|
|
11
7
|
type AgentChatComposerFile,
|
|
12
8
|
} from "@agents24/chat-react/ui"
|
|
13
|
-
import {
|
|
9
|
+
import { type AgentChatInputContract } from "@agents24/chat-react/runtime"
|
|
14
10
|
import type { ContextWindow } from "@agents24/client/protocol"
|
|
15
11
|
|
|
16
12
|
import { Button } from "../ui/button"
|
|
@@ -23,7 +19,9 @@ import { Textarea } from "../ui/textarea"
|
|
|
23
19
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
|
|
24
20
|
import { cn } from "../../lib/utils"
|
|
25
21
|
|
|
26
|
-
type
|
|
22
|
+
export type Agents24ChatComposerProps = {
|
|
23
|
+
queue?: React.ReactNode
|
|
24
|
+
onRecordingStreamChange?: (stream: MediaStream | null) => void
|
|
27
25
|
inputContract: AgentChatInputContract
|
|
28
26
|
disabled?: boolean
|
|
29
27
|
contextWindow?: ContextWindow | null
|
|
@@ -45,6 +43,8 @@ type ChatComposerProps = {
|
|
|
45
43
|
|
|
46
44
|
export function ChatComposer({
|
|
47
45
|
inputContract,
|
|
46
|
+
queue,
|
|
47
|
+
onRecordingStreamChange,
|
|
48
48
|
disabled,
|
|
49
49
|
contextWindow,
|
|
50
50
|
focusKey,
|
|
@@ -61,45 +61,14 @@ export function ChatComposer({
|
|
|
61
61
|
onSubmit,
|
|
62
62
|
restoredDraft,
|
|
63
63
|
utilityRow = "inline",
|
|
64
|
-
}:
|
|
65
|
-
const [text, setText] = React.useState("")
|
|
66
|
-
const [files, setFiles] = React.useState<AgentChatComposerFile[]>([])
|
|
67
|
-
const [attachmentError, setAttachmentError] = React.useState<string | null>(null)
|
|
64
|
+
}: Agents24ChatComposerProps) {
|
|
68
65
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
69
|
-
const textareaRef
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const allowAttachments = attachmentMimeTypes.length > 0
|
|
76
|
-
const recordingAllowed = inputContract.modalities.audio.recording_enabled
|
|
77
|
-
React.useEffect(() => {
|
|
78
|
-
filesRef.current = files
|
|
79
|
-
}, [files])
|
|
80
|
-
React.useEffect(() => () => {
|
|
81
|
-
revokeAgentChatComposerFiles(filesRef.current)
|
|
82
|
-
revokeAgentChatComposerFiles(inFlightFilesRef.current)
|
|
83
|
-
}, [])
|
|
84
|
-
const focusComposer = useChatComposerFocus({ textareaRef, disabled: Boolean(disabled), focusKey, restoredDraft,
|
|
85
|
-
onRestore: (draft) => {
|
|
86
|
-
draftRevisionRef.current += 1
|
|
87
|
-
revokeAgentChatComposerFiles(filesRef.current)
|
|
88
|
-
setText(draft.text)
|
|
89
|
-
setFiles(draft.files)
|
|
90
|
-
} })
|
|
91
|
-
const addRecording = React.useCallback((file: File) => {
|
|
92
|
-
draftRevisionRef.current += 1
|
|
93
|
-
setFiles((current) => [...current, createAgentChatComposerFile(file)])
|
|
94
|
-
}, [])
|
|
95
|
-
const recorder = useAudioRecorder(addRecording)
|
|
96
|
-
const isExpanded = forceExpanded || text.includes("\n") || text.length > 62
|
|
97
|
-
const validationError = validateAgentChatSubmission(
|
|
98
|
-
inputContract,
|
|
99
|
-
text,
|
|
100
|
-
files.map((file) => ({ data: file.source || new Uint8Array(), mediaType: file.mediaType })),
|
|
101
|
-
)
|
|
102
|
-
const action = chatComposerAction({ hasDraft: Boolean(text.trim() || files.length), isRunning, isCancelling })
|
|
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
|
+
})
|
|
103
72
|
|
|
104
73
|
const selectedAgent = agents.find((item) => item.id === agentId)
|
|
105
74
|
const selectedModel = models.find((item) => item.id === modelId)
|
|
@@ -159,65 +128,33 @@ export function ChatComposer({
|
|
|
159
128
|
const hasContextWindow = Number.isFinite(Number(contextWindow?.max_tokens)) && Number(contextWindow?.max_tokens) > 0
|
|
160
129
|
const showUtilityRow = utilityRow === "below"
|
|
161
130
|
|
|
162
|
-
const submit = async () => {
|
|
163
|
-
if (disabled || isCancelling || submittingRef.current || validationError) return
|
|
164
|
-
const value = text
|
|
165
|
-
const selectedFiles = files
|
|
166
|
-
const clearedRevision = draftRevisionRef.current + 1
|
|
167
|
-
draftRevisionRef.current = clearedRevision
|
|
168
|
-
submittingRef.current = true
|
|
169
|
-
inFlightFilesRef.current = selectedFiles
|
|
170
|
-
setText("")
|
|
171
|
-
setFiles([])
|
|
172
|
-
focusComposer()
|
|
173
|
-
try {
|
|
174
|
-
await onSubmit(value, selectedFiles)
|
|
175
|
-
revokeAgentChatComposerFiles(selectedFiles)
|
|
176
|
-
inFlightFilesRef.current = []
|
|
177
|
-
} catch {
|
|
178
|
-
if (draftRevisionRef.current === clearedRevision) {
|
|
179
|
-
draftRevisionRef.current += 1
|
|
180
|
-
setText(value)
|
|
181
|
-
setFiles(selectedFiles)
|
|
182
|
-
inFlightFilesRef.current = []
|
|
183
|
-
} else {
|
|
184
|
-
revokeAgentChatComposerFiles(selectedFiles)
|
|
185
|
-
inFlightFilesRef.current = []
|
|
186
|
-
}
|
|
187
|
-
} finally {
|
|
188
|
-
submittingRef.current = false
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
131
|
return (
|
|
193
132
|
<div
|
|
194
133
|
className="relative z-10 w-full pb-[max(1rem,env(safe-area-inset-bottom))]"
|
|
195
134
|
data-agents24-chat-composer=""
|
|
196
135
|
>
|
|
197
|
-
{
|
|
198
|
-
<div className="mb-2 min-w-0">
|
|
199
|
-
<ChatAttachmentRows
|
|
200
|
-
attachments={files}
|
|
201
|
-
onRemove={(attachment) => {
|
|
202
|
-
if (!attachment.id) return
|
|
203
|
-
draftRevisionRef.current += 1
|
|
204
|
-
setFiles((current) => {
|
|
205
|
-
const removed = current.find((file) => file.id === attachment.id)
|
|
206
|
-
if (removed) revokeAgentChatComposerFiles([removed])
|
|
207
|
-
return current.filter((file) => file.id !== attachment.id)
|
|
208
|
-
})
|
|
209
|
-
}}
|
|
210
|
-
/>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
136
|
+
{queue}
|
|
213
137
|
{attachmentError || recorder.error ? <p className="mb-2 text-xs text-destructive" role="alert">{attachmentError || recorder.error}</p> : null}
|
|
214
138
|
<div
|
|
215
139
|
data-expanded={isExpanded || undefined}
|
|
140
|
+
data-attachments={files.length > 0 || undefined}
|
|
216
141
|
className={cn(
|
|
217
142
|
"agents24-composer-grid grid min-w-0 items-center gap-2 rounded-xl border bg-background shadow-sm",
|
|
218
143
|
isExpanded ? "p-2" : "p-1.5",
|
|
219
144
|
)}
|
|
220
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
|
+
)}
|
|
221
158
|
{allowAttachments ? (
|
|
222
159
|
<>
|
|
223
160
|
<input
|
|
@@ -225,11 +162,9 @@ export function ChatComposer({
|
|
|
225
162
|
className="sr-only"
|
|
226
163
|
type="file"
|
|
227
164
|
multiple
|
|
228
|
-
accept={
|
|
165
|
+
accept={attachmentMimeTypes.join(",")}
|
|
229
166
|
onChange={(event) => {
|
|
230
|
-
|
|
231
|
-
setFiles((current) => [...current, ...selected.map(createAgentChatComposerFile)])
|
|
232
|
-
setAttachmentError(null)
|
|
167
|
+
addFiles(Array.from(event.target.files ?? []))
|
|
233
168
|
event.currentTarget.value = ""
|
|
234
169
|
}}
|
|
235
170
|
/>
|
|
@@ -252,7 +187,6 @@ export function ChatComposer({
|
|
|
252
187
|
isExpanded ? "py-2" : "h-9 py-2",
|
|
253
188
|
)}
|
|
254
189
|
onChange={(event) => {
|
|
255
|
-
draftRevisionRef.current += 1
|
|
256
190
|
setText(event.target.value)
|
|
257
191
|
}}
|
|
258
192
|
onKeyDown={(event) => {
|
|
@@ -291,7 +225,7 @@ export function ChatComposer({
|
|
|
291
225
|
<TooltipContent>{validationError}</TooltipContent>
|
|
292
226
|
</Tooltip>
|
|
293
227
|
) : (
|
|
294
|
-
<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>
|
|
295
229
|
)}
|
|
296
230
|
</div>
|
|
297
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"
|
|
@@ -92,6 +93,7 @@ export function ChatShell({
|
|
|
92
93
|
runtime: AgentChatRuntime
|
|
93
94
|
sidebar?: React.ReactNode
|
|
94
95
|
}) {
|
|
96
|
+
const Composer = components?.Composer || ChatComposer
|
|
95
97
|
const { controller } = runtime
|
|
96
98
|
const isUploading = controller.runState === "uploading"
|
|
97
99
|
const isRunning =
|
|
@@ -167,6 +169,7 @@ export function ChatShell({
|
|
|
167
169
|
data-agents24-chat-transcript=""
|
|
168
170
|
>
|
|
169
171
|
<OperationErrorNotice runtime={runtime} />
|
|
172
|
+
<SubagentTasks threadId={controller.activeThreadId} />
|
|
170
173
|
<LatestThreadScroller.Provider autoScroll defaultScrollPosition="last-anchor" scrollPreviousItemPeek={64}>
|
|
171
174
|
{!isEmptyState && (
|
|
172
175
|
<LatestThreadScroller.Root className="relative min-h-0 flex-1">
|
|
@@ -239,11 +242,10 @@ export function ChatShell({
|
|
|
239
242
|
<LatestThreadScroller.Button className="!relative !inset-auto !translate-x-0 rtl:!translate-x-0" />
|
|
240
243
|
</div>}
|
|
241
244
|
<div className="pointer-events-auto mx-auto w-full max-w-3xl px-4 sm:px-6">
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
onEdit={retractForEdit} onResume={controller.resumeQueue} />
|
|
246
|
-
<ChatComposer
|
|
245
|
+
<Composer
|
|
246
|
+
queue={<AgentChatQueue pending={controller.pendingMessages} queue={controller.messageQueue}
|
|
247
|
+
onRemove={controller.removePendingMessage}
|
|
248
|
+
onEdit={retractForEdit} onResume={controller.resumeQueue} />}
|
|
247
249
|
restoredDraft={restoredDraft}
|
|
248
250
|
inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
|
|
249
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
|
+
}
|
|
@@ -11,7 +11,7 @@ const options = [
|
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
export function ThemeMenu() {
|
|
14
|
-
const { appearance, setAppearance } = useTheme()
|
|
14
|
+
const { appearance, setAppearance, inherited } = useTheme()
|
|
15
15
|
const [open, setOpen] = React.useState(false)
|
|
16
16
|
const rootRef = React.useRef<HTMLDivElement>(null)
|
|
17
17
|
const firstItemRef = React.useRef<HTMLButtonElement>(null)
|
|
@@ -35,6 +35,8 @@ export function ThemeMenu() {
|
|
|
35
35
|
}
|
|
36
36
|
}, [open])
|
|
37
37
|
|
|
38
|
+
if (inherited) return null
|
|
39
|
+
|
|
38
40
|
const label = appearance === "system" ? "System" : appearance === "light" ? "Light" : "Dark"
|
|
39
41
|
return (
|
|
40
42
|
<div ref={rootRef} className="relative">
|