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.
Files changed (138) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/LICENSE +7 -0
  3. package/README.md +235 -3
  4. package/dist/a2ui/views.js +2 -2
  5. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  6. package/dist/agent-chat/a2ui-block.js +69 -0
  7. package/dist/agent-chat/a2ui-block.js.map +1 -0
  8. package/dist/agent-chat/agui-client.d.ts +40 -0
  9. package/dist/agent-chat/agui-client.js +251 -0
  10. package/dist/agent-chat/agui-client.js.map +1 -0
  11. package/dist/agent-chat/bridge.d.ts +109 -0
  12. package/dist/agent-chat/bridge.js +353 -0
  13. package/dist/agent-chat/bridge.js.map +1 -0
  14. package/dist/agent-chat/session.d.ts +79 -0
  15. package/dist/agent-chat/session.js +391 -0
  16. package/dist/agent-chat/session.js.map +1 -0
  17. package/dist/agent-chat/step-label.d.ts +99 -0
  18. package/dist/agent-chat/step-label.js +116 -0
  19. package/dist/agent-chat/step-label.js.map +1 -0
  20. package/dist/agent-chat/store.d.ts +102 -0
  21. package/dist/agent-chat/store.js +876 -0
  22. package/dist/agent-chat/store.js.map +1 -0
  23. package/dist/agent-chat/types.d.ts +277 -0
  24. package/dist/agent-chat/types.js +17 -0
  25. package/dist/agent-chat/types.js.map +1 -0
  26. package/dist/agent-chat.d.ts +11 -0
  27. package/dist/agent-chat.js +11 -0
  28. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  29. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  30. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  31. package/dist/components/admin-permissions/context.d.ts +70 -0
  32. package/dist/components/admin-permissions/context.js +258 -0
  33. package/dist/components/admin-permissions/context.js.map +1 -0
  34. package/dist/components/admin-permissions/index.d.ts +10 -0
  35. package/dist/components/admin-permissions/licence.d.ts +15 -0
  36. package/dist/components/admin-permissions/licence.js +78 -0
  37. package/dist/components/admin-permissions/licence.js.map +1 -0
  38. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  39. package/dist/components/admin-permissions/matrix.js +191 -0
  40. package/dist/components/admin-permissions/matrix.js.map +1 -0
  41. package/dist/components/admin-permissions/members.d.ts +18 -0
  42. package/dist/components/admin-permissions/members.js +185 -0
  43. package/dist/components/admin-permissions/members.js.map +1 -0
  44. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  45. package/dist/components/admin-permissions/role-assignment.js +174 -0
  46. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  47. package/dist/components/admin-permissions/roles.d.ts +25 -0
  48. package/dist/components/admin-permissions/roles.js +168 -0
  49. package/dist/components/admin-permissions/roles.js.map +1 -0
  50. package/dist/components/admin-permissions/types.d.ts +152 -0
  51. package/dist/components/admin-permissions/types.js +63 -0
  52. package/dist/components/admin-permissions/types.js.map +1 -0
  53. package/dist/components/agent-chat-popup.d.ts +29 -0
  54. package/dist/components/agent-chat-popup.js +188 -0
  55. package/dist/components/agent-chat-popup.js.map +1 -0
  56. package/dist/components/agent-chat.d.ts +163 -0
  57. package/dist/components/agent-chat.js +673 -0
  58. package/dist/components/agent-chat.js.map +1 -0
  59. package/dist/components/app-shell.d.ts +126 -0
  60. package/dist/components/app-shell.js +297 -0
  61. package/dist/components/app-shell.js.map +1 -0
  62. package/dist/components/badge.d.ts +1 -1
  63. package/dist/components/button-link.js +1 -1
  64. package/dist/components/button.d.ts +1 -1
  65. package/dist/components/checkbox.d.ts +1 -1
  66. package/dist/components/combobox.d.ts +1 -1
  67. package/dist/components/combobox.js +1 -1
  68. package/dist/components/consent-screen.d.ts +65 -0
  69. package/dist/components/consent-screen.js +123 -0
  70. package/dist/components/consent-screen.js.map +1 -0
  71. package/dist/components/data-table/data-table.d.ts +15 -1
  72. package/dist/components/data-table/data-table.js +18 -4
  73. package/dist/components/data-table/data-table.js.map +1 -1
  74. package/dist/components/data-table/index.d.ts +4 -4
  75. package/dist/components/data-table/parts.d.ts +27 -3
  76. package/dist/components/data-table/parts.js +175 -55
  77. package/dist/components/data-table/parts.js.map +1 -1
  78. package/dist/components/data-table/types.d.ts +61 -0
  79. package/dist/components/data-table/use-data-table.js +91 -6
  80. package/dist/components/data-table/use-data-table.js.map +1 -1
  81. package/dist/components/data-table/use-server-source.js +119 -28
  82. package/dist/components/data-table/use-server-source.js.map +1 -1
  83. package/dist/components/help-panel.d.ts +131 -0
  84. package/dist/components/help-panel.js +545 -0
  85. package/dist/components/help-panel.js.map +1 -0
  86. package/dist/components/login-screen.d.ts +127 -0
  87. package/dist/components/login-screen.js +339 -0
  88. package/dist/components/login-screen.js.map +1 -0
  89. package/dist/components/session-guard.d.ts +268 -0
  90. package/dist/components/session-guard.js +632 -0
  91. package/dist/components/session-guard.js.map +1 -0
  92. package/dist/components/toast.d.ts +1 -1
  93. package/dist/core.d.ts +5 -1
  94. package/dist/core.js +11 -7
  95. package/dist/data-table.d.ts +13 -4
  96. package/dist/data-table.js +10 -2
  97. package/dist/hooks/use-cortena-theme.js +49 -3
  98. package/dist/hooks/use-cortena-theme.js.map +1 -1
  99. package/dist/index.d.ts +17 -4
  100. package/dist/index.js +21 -8
  101. package/dist/markdown.d.ts +2 -1
  102. package/dist/markdown.js +2 -1
  103. package/package.json +18 -5
  104. package/src/agent-chat/a2ui-block.ts +118 -0
  105. package/src/agent-chat/agui-client.ts +405 -0
  106. package/src/agent-chat/bridge.ts +445 -0
  107. package/src/agent-chat/session.ts +549 -0
  108. package/src/agent-chat/step-label.ts +177 -0
  109. package/src/agent-chat/store.ts +1234 -0
  110. package/src/agent-chat/types.ts +308 -0
  111. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  112. package/src/components/admin-permissions/context.tsx +376 -0
  113. package/src/components/admin-permissions/index.tsx +32 -0
  114. package/src/components/admin-permissions/licence.tsx +84 -0
  115. package/src/components/admin-permissions/matrix.tsx +257 -0
  116. package/src/components/admin-permissions/members.tsx +204 -0
  117. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  118. package/src/components/admin-permissions/roles.tsx +169 -0
  119. package/src/components/admin-permissions/types.ts +231 -0
  120. package/src/components/agent-chat-popup.tsx +289 -0
  121. package/src/components/agent-chat.tsx +1006 -0
  122. package/src/components/app-shell.tsx +502 -0
  123. package/src/components/consent-screen.tsx +239 -0
  124. package/src/components/data-table/data-table.tsx +36 -0
  125. package/src/components/data-table/index.tsx +6 -1
  126. package/src/components/data-table/parts.tsx +223 -47
  127. package/src/components/data-table/types.ts +68 -0
  128. package/src/components/data-table/use-data-table.ts +152 -4
  129. package/src/components/data-table/use-server-source.ts +150 -12
  130. package/src/components/help-panel.tsx +765 -0
  131. package/src/components/login-screen.tsx +479 -0
  132. package/src/components/session-guard.tsx +1071 -0
  133. package/src/entries/agent-chat.ts +137 -0
  134. package/src/entries/core.ts +8 -0
  135. package/src/entries/data-table.ts +41 -0
  136. package/src/entries/markdown.ts +25 -0
  137. package/src/hooks/use-cortena-theme.ts +63 -4
  138. 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 };