cortena-ui 1.4.2 → 1.6.0
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/CHANGELOG.md +107 -0
- package/LICENSE +7 -0
- package/README.md +235 -3
- package/dist/a2ui/views.js +2 -2
- package/dist/agent-chat/a2ui-block.d.ts +60 -0
- package/dist/agent-chat/a2ui-block.js +69 -0
- package/dist/agent-chat/a2ui-block.js.map +1 -0
- package/dist/agent-chat/agui-client.d.ts +40 -0
- package/dist/agent-chat/agui-client.js +251 -0
- package/dist/agent-chat/agui-client.js.map +1 -0
- package/dist/agent-chat/bridge.d.ts +109 -0
- package/dist/agent-chat/bridge.js +353 -0
- package/dist/agent-chat/bridge.js.map +1 -0
- package/dist/agent-chat/session.d.ts +79 -0
- package/dist/agent-chat/session.js +391 -0
- package/dist/agent-chat/session.js.map +1 -0
- package/dist/agent-chat/step-label.d.ts +99 -0
- package/dist/agent-chat/step-label.js +116 -0
- package/dist/agent-chat/step-label.js.map +1 -0
- package/dist/agent-chat/store.d.ts +102 -0
- package/dist/agent-chat/store.js +876 -0
- package/dist/agent-chat/store.js.map +1 -0
- package/dist/agent-chat/types.d.ts +277 -0
- package/dist/agent-chat/types.js +17 -0
- package/dist/agent-chat/types.js.map +1 -0
- package/dist/agent-chat.d.ts +11 -0
- package/dist/agent-chat.js +11 -0
- package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
- package/dist/components/admin-permissions/admin-permissions.js +101 -0
- package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
- package/dist/components/admin-permissions/context.d.ts +70 -0
- package/dist/components/admin-permissions/context.js +258 -0
- package/dist/components/admin-permissions/context.js.map +1 -0
- package/dist/components/admin-permissions/index.d.ts +10 -0
- package/dist/components/admin-permissions/licence.d.ts +15 -0
- package/dist/components/admin-permissions/licence.js +78 -0
- package/dist/components/admin-permissions/licence.js.map +1 -0
- package/dist/components/admin-permissions/matrix.d.ts +20 -0
- package/dist/components/admin-permissions/matrix.js +191 -0
- package/dist/components/admin-permissions/matrix.js.map +1 -0
- package/dist/components/admin-permissions/members.d.ts +18 -0
- package/dist/components/admin-permissions/members.js +185 -0
- package/dist/components/admin-permissions/members.js.map +1 -0
- package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
- package/dist/components/admin-permissions/role-assignment.js +174 -0
- package/dist/components/admin-permissions/role-assignment.js.map +1 -0
- package/dist/components/admin-permissions/roles.d.ts +25 -0
- package/dist/components/admin-permissions/roles.js +168 -0
- package/dist/components/admin-permissions/roles.js.map +1 -0
- package/dist/components/admin-permissions/types.d.ts +152 -0
- package/dist/components/admin-permissions/types.js +63 -0
- package/dist/components/admin-permissions/types.js.map +1 -0
- package/dist/components/agent-chat-popup.d.ts +29 -0
- package/dist/components/agent-chat-popup.js +188 -0
- package/dist/components/agent-chat-popup.js.map +1 -0
- package/dist/components/agent-chat.d.ts +163 -0
- package/dist/components/agent-chat.js +673 -0
- package/dist/components/agent-chat.js.map +1 -0
- package/dist/components/app-shell.d.ts +126 -0
- package/dist/components/app-shell.js +297 -0
- package/dist/components/app-shell.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button-link.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/consent-screen.d.ts +65 -0
- package/dist/components/consent-screen.js +123 -0
- package/dist/components/consent-screen.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts +15 -1
- package/dist/components/data-table/data-table.js +18 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/index.d.ts +4 -4
- package/dist/components/data-table/parts.d.ts +27 -3
- package/dist/components/data-table/parts.js +175 -55
- package/dist/components/data-table/parts.js.map +1 -1
- package/dist/components/data-table/types.d.ts +61 -0
- package/dist/components/data-table/use-data-table.js +91 -6
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/components/data-table/use-server-source.js +119 -28
- package/dist/components/data-table/use-server-source.js.map +1 -1
- package/dist/components/help-panel.d.ts +131 -0
- package/dist/components/help-panel.js +545 -0
- package/dist/components/help-panel.js.map +1 -0
- package/dist/components/login-screen.d.ts +127 -0
- package/dist/components/login-screen.js +339 -0
- package/dist/components/login-screen.js.map +1 -0
- package/dist/components/session-guard.d.ts +268 -0
- package/dist/components/session-guard.js +632 -0
- package/dist/components/session-guard.js.map +1 -0
- package/dist/components/toast.d.ts +1 -1
- package/dist/core.d.ts +5 -1
- package/dist/core.js +11 -7
- package/dist/data-table.d.ts +13 -4
- package/dist/data-table.js +10 -2
- package/dist/hooks/use-cortena-theme.js +49 -3
- package/dist/hooks/use-cortena-theme.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +21 -8
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -1
- package/package.json +18 -5
- package/src/agent-chat/a2ui-block.ts +118 -0
- package/src/agent-chat/agui-client.ts +405 -0
- package/src/agent-chat/bridge.ts +445 -0
- package/src/agent-chat/session.ts +549 -0
- package/src/agent-chat/step-label.ts +177 -0
- package/src/agent-chat/store.ts +1234 -0
- package/src/agent-chat/types.ts +308 -0
- package/src/components/admin-permissions/admin-permissions.tsx +130 -0
- package/src/components/admin-permissions/context.tsx +376 -0
- package/src/components/admin-permissions/index.tsx +32 -0
- package/src/components/admin-permissions/licence.tsx +84 -0
- package/src/components/admin-permissions/matrix.tsx +257 -0
- package/src/components/admin-permissions/members.tsx +204 -0
- package/src/components/admin-permissions/role-assignment.tsx +239 -0
- package/src/components/admin-permissions/roles.tsx +169 -0
- package/src/components/admin-permissions/types.ts +231 -0
- package/src/components/agent-chat-popup.tsx +289 -0
- package/src/components/agent-chat.tsx +1006 -0
- package/src/components/app-shell.tsx +502 -0
- package/src/components/consent-screen.tsx +239 -0
- package/src/components/data-table/data-table.tsx +36 -0
- package/src/components/data-table/index.tsx +6 -1
- package/src/components/data-table/parts.tsx +223 -47
- package/src/components/data-table/types.ts +68 -0
- package/src/components/data-table/use-data-table.ts +152 -4
- package/src/components/data-table/use-server-source.ts +150 -12
- package/src/components/help-panel.tsx +765 -0
- package/src/components/login-screen.tsx +479 -0
- package/src/components/session-guard.tsx +1071 -0
- package/src/entries/agent-chat.ts +137 -0
- package/src/entries/core.ts +8 -0
- package/src/entries/data-table.ts +41 -0
- package/src/entries/markdown.ts +25 -0
- package/src/hooks/use-cortena-theme.ts +63 -4
- package/src/index.ts +6 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentChatPopup — the extension's own agent, in the corner of every screen.
|
|
5
|
+
*
|
|
6
|
+
* §17 of how-to-create-a-cortena-extension, audit rule P-25. Mounted in
|
|
7
|
+
* `AppShell`'s `agentSlot`, so it stacks above the theme/help cluster rather
|
|
8
|
+
* than beside it and no extension positions a floating control itself.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <AppShell
|
|
12
|
+
* extension={{ id: "tasks", name: "Tasks" }}
|
|
13
|
+
* user={user}
|
|
14
|
+
* agentSlot={
|
|
15
|
+
* <AgentChatPopup
|
|
16
|
+
* extension={{ id: "tasks", name: "Tasks" }}
|
|
17
|
+
* client={client} // createAguiAgentChatClient({ …, agentId: "tasks" })
|
|
18
|
+
* />
|
|
19
|
+
* }
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Three states, and the transitions between them are fixed: a collapsed pill,
|
|
24
|
+
* a panel, and full screen. **Cmd/Ctrl+J** toggles between the pill and the
|
|
25
|
+
* panel; **Escape** steps full screen -> panel -> pill, except while a run is
|
|
26
|
+
* streaming, where the composer takes Escape to mean "stop" — the first Escape
|
|
27
|
+
* during a run is nearly always about the run, not about the window.
|
|
28
|
+
*
|
|
29
|
+
* The chat stays mounted in every state, hidden rather than unmounted while
|
|
30
|
+
* collapsed. That is not an optimisation: a run started before the user
|
|
31
|
+
* collapsed the pop-up keeps streaming into it, and the unread dot on the pill
|
|
32
|
+
* is what says so. Unmounting would throw the reply away halfway through.
|
|
33
|
+
*
|
|
34
|
+
* State is remembered per window, in `sessionStorage`, alongside the session
|
|
35
|
+
* key — a second window of the same extension opens its own chat on its own
|
|
36
|
+
* session rather than inheriting this one's.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { Maximize2, Minimize2, X } from "lucide-react";
|
|
40
|
+
import * as React from "react";
|
|
41
|
+
import { BrandMark } from "@/components/app-shell";
|
|
42
|
+
import { Button } from "@/components/button";
|
|
43
|
+
import { AgentChat, type AgentChatProps } from "@/components/agent-chat";
|
|
44
|
+
import { viewStorageKey, windowSessionStorage } from "@/agent-chat/session";
|
|
45
|
+
import type { AgentChatClient } from "@/agent-chat/types";
|
|
46
|
+
import { cn } from "@/lib/cn";
|
|
47
|
+
|
|
48
|
+
export type AgentChatPopupState = "pill" | "panel" | "full";
|
|
49
|
+
|
|
50
|
+
export interface AgentChatPopupExtension {
|
|
51
|
+
/** Registered mark id — the same `x-cortena.icon` the shell draws. */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Display name. Also the agent template slug, unless `agentId` overrides it. */
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AgentChatPopupProps
|
|
58
|
+
extends Omit<
|
|
59
|
+
AgentChatProps,
|
|
60
|
+
"agentName" | "storageScope" | "className" | "headerSlot" | "client"
|
|
61
|
+
> {
|
|
62
|
+
extension: AgentChatPopupExtension;
|
|
63
|
+
/** The transport. Its `agentId` is the template slug the sessions belong to. */
|
|
64
|
+
client: AgentChatClient;
|
|
65
|
+
/**
|
|
66
|
+
* Names the `sessionStorage` slots. Defaults to the extension id, which is
|
|
67
|
+
* what keeps two extensions on one origin out of each other's sessions.
|
|
68
|
+
*/
|
|
69
|
+
storageScope?: string;
|
|
70
|
+
/** Start here instead of collapsed; the stored state still wins on reload. */
|
|
71
|
+
defaultState?: AgentChatPopupState;
|
|
72
|
+
className?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Cmd on a Mac, Ctrl elsewhere. Documented in README.md. */
|
|
76
|
+
function isToggleShortcut(event: KeyboardEvent): boolean {
|
|
77
|
+
return (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === "j";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function AgentChatPopup({
|
|
81
|
+
extension,
|
|
82
|
+
client,
|
|
83
|
+
storageScope,
|
|
84
|
+
defaultState = "pill",
|
|
85
|
+
className,
|
|
86
|
+
...chatProps
|
|
87
|
+
}: AgentChatPopupProps) {
|
|
88
|
+
const scope = storageScope ?? extension.id;
|
|
89
|
+
const [state, setState] = React.useState<AgentChatPopupState>(defaultState);
|
|
90
|
+
const [unread, setUnread] = React.useState(false);
|
|
91
|
+
const stateRef = React.useRef(state);
|
|
92
|
+
stateRef.current = state;
|
|
93
|
+
const pillRef = React.useRef<HTMLButtonElement>(null);
|
|
94
|
+
const panelRef = React.useRef<HTMLDivElement>(null);
|
|
95
|
+
/** Whether the user has opened or closed it, as against it starting open. */
|
|
96
|
+
const moved = React.useRef(false);
|
|
97
|
+
|
|
98
|
+
// Restore this window's open/collapsed choice. In an effect rather than a
|
|
99
|
+
// lazy initialiser so a server render and the first client render agree.
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
try {
|
|
102
|
+
const stored = windowSessionStorage()?.getItem(viewStorageKey(scope));
|
|
103
|
+
if (stored === "pill" || stored === "panel" || stored === "full") {
|
|
104
|
+
setState(stored);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// A blocked storage just means the pop-up opens collapsed.
|
|
108
|
+
}
|
|
109
|
+
}, [scope]);
|
|
110
|
+
|
|
111
|
+
const go = React.useCallback(
|
|
112
|
+
(next: AgentChatPopupState) => {
|
|
113
|
+
moved.current = true;
|
|
114
|
+
setState(next);
|
|
115
|
+
if (next !== "pill") {
|
|
116
|
+
setUnread(false);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
windowSessionStorage()?.setItem(viewStorageKey(scope), next);
|
|
120
|
+
} catch {
|
|
121
|
+
// Not remembering the choice is not a reason to refuse the change.
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[scope],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// The unread dot. Subscribed here rather than read out of the chat, because
|
|
128
|
+
// the point is what happened while the surface was not being looked at.
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
return client.onChat((event) => {
|
|
131
|
+
if (stateRef.current === "pill" && (event.state === "delta" || event.state === "final")) {
|
|
132
|
+
setUnread(true);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}, [client]);
|
|
136
|
+
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
139
|
+
if (!isToggleShortcut(event)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
go(stateRef.current === "pill" ? "panel" : "pill");
|
|
144
|
+
};
|
|
145
|
+
window.addEventListener("keydown", onKeyDown);
|
|
146
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
147
|
+
}, [go]);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Escape steps down one level. It is handled on the panel rather than on the
|
|
151
|
+
* window so it only fires when focus is inside the pop-up, and it arrives
|
|
152
|
+
* here only when the composer did not claim it — the composer stops
|
|
153
|
+
* propagation while a run is streaming, where Escape means "stop the run".
|
|
154
|
+
*/
|
|
155
|
+
const onKeyDown = React.useCallback(
|
|
156
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
157
|
+
if (event.key !== "Escape") {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
go(stateRef.current === "full" ? "panel" : "pill");
|
|
162
|
+
},
|
|
163
|
+
[go],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const open = state !== "pill";
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Focus follows the pop-up.
|
|
170
|
+
*
|
|
171
|
+
* The panel is `hidden`, not unmounted, so nothing moves focus by itself.
|
|
172
|
+
* Opening left focus on the pill — which is now `hidden`, so the browser
|
|
173
|
+
* resets it to <body> and the next Tab starts at the top of the page rather
|
|
174
|
+
* than in the chat that was just opened. Closing had the mirror problem:
|
|
175
|
+
* focus was inside a subtree that had just become hidden.
|
|
176
|
+
*
|
|
177
|
+
* Only after the user moves it. A pop-up that starts on `panel`, or one
|
|
178
|
+
* restored from `sessionStorage` on load, must not steal focus from the page.
|
|
179
|
+
*/
|
|
180
|
+
React.useEffect(() => {
|
|
181
|
+
if (!moved.current) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const frame = requestAnimationFrame(() => {
|
|
185
|
+
if (open) {
|
|
186
|
+
panelRef.current
|
|
187
|
+
?.querySelector<HTMLElement>('[data-slot="agent-chat-input"]')
|
|
188
|
+
?.focus({ preventScroll: true });
|
|
189
|
+
} else {
|
|
190
|
+
pillRef.current?.focus({ preventScroll: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return () => cancelAnimationFrame(frame);
|
|
194
|
+
}, [open]);
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
<button
|
|
199
|
+
ref={pillRef}
|
|
200
|
+
type="button"
|
|
201
|
+
data-slot="agent-chat-pill"
|
|
202
|
+
data-state={state}
|
|
203
|
+
data-unread={unread ? "" : undefined}
|
|
204
|
+
aria-expanded={open}
|
|
205
|
+
aria-label={`Ask ${extension.name}`}
|
|
206
|
+
hidden={open}
|
|
207
|
+
onClick={() => go("panel")}
|
|
208
|
+
className={cn(
|
|
209
|
+
"relative inline-flex h-10 items-center gap-2 rounded-[var(--ds-radius-full)] px-3.5",
|
|
210
|
+
"border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]",
|
|
211
|
+
"shadow-[var(--ds-shadow-md)] outline-none",
|
|
212
|
+
"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]",
|
|
213
|
+
"hover:bg-[var(--ds-hover)]",
|
|
214
|
+
"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2",
|
|
215
|
+
"focus-visible:ring-offset-[var(--ds-background)]",
|
|
216
|
+
className,
|
|
217
|
+
)}
|
|
218
|
+
>
|
|
219
|
+
<span className="flex size-5 items-center justify-center text-[color:var(--ds-primary)]">
|
|
220
|
+
<BrandMark id={extension.id} name={extension.name} size={16} decorative />
|
|
221
|
+
</span>
|
|
222
|
+
<span className="text-[length:var(--ds-text-caption-lg)] font-medium">
|
|
223
|
+
Ask {extension.name}
|
|
224
|
+
</span>
|
|
225
|
+
{unread ? (
|
|
226
|
+
<span
|
|
227
|
+
data-slot="agent-chat-pill-unread"
|
|
228
|
+
aria-label="The agent is replying"
|
|
229
|
+
className="size-2 rounded-[var(--ds-radius-full)] bg-[var(--ds-primary)]"
|
|
230
|
+
/>
|
|
231
|
+
) : null}
|
|
232
|
+
</button>
|
|
233
|
+
|
|
234
|
+
{/* Hidden, not unmounted: a run started before the pop-up was collapsed
|
|
235
|
+
keeps streaming, and the pill's dot is what reports it. */}
|
|
236
|
+
<div
|
|
237
|
+
ref={panelRef}
|
|
238
|
+
data-slot="agent-chat-panel"
|
|
239
|
+
data-state={state}
|
|
240
|
+
role="dialog"
|
|
241
|
+
aria-label={`${extension.name} agent`}
|
|
242
|
+
hidden={!open}
|
|
243
|
+
onKeyDown={onKeyDown}
|
|
244
|
+
className={cn(
|
|
245
|
+
"fixed z-50 flex flex-col overflow-hidden border border-[var(--ds-border)]",
|
|
246
|
+
"bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]",
|
|
247
|
+
state === "full"
|
|
248
|
+
? "inset-0 rounded-none"
|
|
249
|
+
: cn(
|
|
250
|
+
"right-6 bottom-6 rounded-[var(--ds-radius-lg)]",
|
|
251
|
+
"h-[min(40rem,calc(100vh-3rem))] w-[min(26.25rem,calc(100vw-3rem))]",
|
|
252
|
+
),
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
<AgentChat
|
|
256
|
+
client={client}
|
|
257
|
+
agentName={extension.name}
|
|
258
|
+
storageScope={scope}
|
|
259
|
+
className="min-h-0 flex-1"
|
|
260
|
+
headerSlot={
|
|
261
|
+
<>
|
|
262
|
+
<Button
|
|
263
|
+
size="icon-sm"
|
|
264
|
+
variant="ghost"
|
|
265
|
+
data-slot="agent-chat-expand"
|
|
266
|
+
aria-label={state === "full" ? "Restore the panel" : "Expand to full screen"}
|
|
267
|
+
onClick={() => go(state === "full" ? "panel" : "full")}
|
|
268
|
+
>
|
|
269
|
+
{state === "full" ? <Minimize2 aria-hidden /> : <Maximize2 aria-hidden />}
|
|
270
|
+
</Button>
|
|
271
|
+
<Button
|
|
272
|
+
size="icon-sm"
|
|
273
|
+
variant="ghost"
|
|
274
|
+
data-slot="agent-chat-collapse"
|
|
275
|
+
aria-label="Collapse the agent"
|
|
276
|
+
onClick={() => go("pill")}
|
|
277
|
+
>
|
|
278
|
+
<X aria-hidden />
|
|
279
|
+
</Button>
|
|
280
|
+
</>
|
|
281
|
+
}
|
|
282
|
+
{...chatProps}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
</>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export { AgentChatPopup };
|