cortena-ui 1.4.2 → 1.5.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 (133) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +149 -1
  3. package/dist/a2ui/views.js +2 -2
  4. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  5. package/dist/agent-chat/a2ui-block.js +69 -0
  6. package/dist/agent-chat/a2ui-block.js.map +1 -0
  7. package/dist/agent-chat/agui-client.d.ts +40 -0
  8. package/dist/agent-chat/agui-client.js +251 -0
  9. package/dist/agent-chat/agui-client.js.map +1 -0
  10. package/dist/agent-chat/bridge.d.ts +109 -0
  11. package/dist/agent-chat/bridge.js +349 -0
  12. package/dist/agent-chat/bridge.js.map +1 -0
  13. package/dist/agent-chat/session.d.ts +58 -0
  14. package/dist/agent-chat/session.js +249 -0
  15. package/dist/agent-chat/session.js.map +1 -0
  16. package/dist/agent-chat/store.d.ts +67 -0
  17. package/dist/agent-chat/store.js +548 -0
  18. package/dist/agent-chat/store.js.map +1 -0
  19. package/dist/agent-chat/types.d.ts +187 -0
  20. package/dist/agent-chat/types.js +17 -0
  21. package/dist/agent-chat/types.js.map +1 -0
  22. package/dist/agent-chat.d.ts +10 -0
  23. package/dist/agent-chat.js +10 -0
  24. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  25. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  26. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  27. package/dist/components/admin-permissions/context.d.ts +70 -0
  28. package/dist/components/admin-permissions/context.js +258 -0
  29. package/dist/components/admin-permissions/context.js.map +1 -0
  30. package/dist/components/admin-permissions/index.d.ts +10 -0
  31. package/dist/components/admin-permissions/licence.d.ts +15 -0
  32. package/dist/components/admin-permissions/licence.js +78 -0
  33. package/dist/components/admin-permissions/licence.js.map +1 -0
  34. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  35. package/dist/components/admin-permissions/matrix.js +191 -0
  36. package/dist/components/admin-permissions/matrix.js.map +1 -0
  37. package/dist/components/admin-permissions/members.d.ts +18 -0
  38. package/dist/components/admin-permissions/members.js +185 -0
  39. package/dist/components/admin-permissions/members.js.map +1 -0
  40. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  41. package/dist/components/admin-permissions/role-assignment.js +174 -0
  42. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  43. package/dist/components/admin-permissions/roles.d.ts +25 -0
  44. package/dist/components/admin-permissions/roles.js +168 -0
  45. package/dist/components/admin-permissions/roles.js.map +1 -0
  46. package/dist/components/admin-permissions/types.d.ts +152 -0
  47. package/dist/components/admin-permissions/types.js +63 -0
  48. package/dist/components/admin-permissions/types.js.map +1 -0
  49. package/dist/components/agent-chat-popup.d.ts +29 -0
  50. package/dist/components/agent-chat-popup.js +188 -0
  51. package/dist/components/agent-chat-popup.js.map +1 -0
  52. package/dist/components/agent-chat.d.ts +143 -0
  53. package/dist/components/agent-chat.js +578 -0
  54. package/dist/components/agent-chat.js.map +1 -0
  55. package/dist/components/app-shell.d.ts +126 -0
  56. package/dist/components/app-shell.js +297 -0
  57. package/dist/components/app-shell.js.map +1 -0
  58. package/dist/components/badge.d.ts +1 -1
  59. package/dist/components/button-link.js +1 -1
  60. package/dist/components/button.d.ts +2 -2
  61. package/dist/components/checkbox.d.ts +1 -1
  62. package/dist/components/combobox.d.ts +1 -1
  63. package/dist/components/combobox.js +1 -1
  64. package/dist/components/consent-screen.d.ts +65 -0
  65. package/dist/components/consent-screen.js +123 -0
  66. package/dist/components/consent-screen.js.map +1 -0
  67. package/dist/components/data-table/data-table.d.ts +15 -1
  68. package/dist/components/data-table/data-table.js +18 -4
  69. package/dist/components/data-table/data-table.js.map +1 -1
  70. package/dist/components/data-table/index.d.ts +4 -4
  71. package/dist/components/data-table/parts.d.ts +27 -3
  72. package/dist/components/data-table/parts.js +175 -55
  73. package/dist/components/data-table/parts.js.map +1 -1
  74. package/dist/components/data-table/types.d.ts +61 -0
  75. package/dist/components/data-table/use-data-table.js +91 -6
  76. package/dist/components/data-table/use-data-table.js.map +1 -1
  77. package/dist/components/data-table/use-server-source.js +119 -28
  78. package/dist/components/data-table/use-server-source.js.map +1 -1
  79. package/dist/components/help-panel.d.ts +131 -0
  80. package/dist/components/help-panel.js +545 -0
  81. package/dist/components/help-panel.js.map +1 -0
  82. package/dist/components/login-screen.d.ts +127 -0
  83. package/dist/components/login-screen.js +339 -0
  84. package/dist/components/login-screen.js.map +1 -0
  85. package/dist/components/session-guard.d.ts +268 -0
  86. package/dist/components/session-guard.js +632 -0
  87. package/dist/components/session-guard.js.map +1 -0
  88. package/dist/components/toast.d.ts +1 -1
  89. package/dist/core.d.ts +5 -1
  90. package/dist/core.js +11 -7
  91. package/dist/data-table.d.ts +13 -4
  92. package/dist/data-table.js +10 -2
  93. package/dist/hooks/use-cortena-theme.js +49 -3
  94. package/dist/hooks/use-cortena-theme.js.map +1 -1
  95. package/dist/index.d.ts +17 -4
  96. package/dist/index.js +21 -8
  97. package/dist/markdown.d.ts +2 -1
  98. package/dist/markdown.js +2 -1
  99. package/package.json +16 -4
  100. package/src/agent-chat/a2ui-block.ts +118 -0
  101. package/src/agent-chat/agui-client.ts +405 -0
  102. package/src/agent-chat/bridge.ts +433 -0
  103. package/src/agent-chat/session.ts +392 -0
  104. package/src/agent-chat/store.ts +738 -0
  105. package/src/agent-chat/types.ts +213 -0
  106. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  107. package/src/components/admin-permissions/context.tsx +376 -0
  108. package/src/components/admin-permissions/index.tsx +32 -0
  109. package/src/components/admin-permissions/licence.tsx +84 -0
  110. package/src/components/admin-permissions/matrix.tsx +257 -0
  111. package/src/components/admin-permissions/members.tsx +204 -0
  112. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  113. package/src/components/admin-permissions/roles.tsx +169 -0
  114. package/src/components/admin-permissions/types.ts +231 -0
  115. package/src/components/agent-chat-popup.tsx +289 -0
  116. package/src/components/agent-chat.tsx +843 -0
  117. package/src/components/app-shell.tsx +502 -0
  118. package/src/components/consent-screen.tsx +239 -0
  119. package/src/components/data-table/data-table.tsx +36 -0
  120. package/src/components/data-table/index.tsx +6 -1
  121. package/src/components/data-table/parts.tsx +223 -47
  122. package/src/components/data-table/types.ts +68 -0
  123. package/src/components/data-table/use-data-table.ts +152 -4
  124. package/src/components/data-table/use-server-source.ts +150 -12
  125. package/src/components/help-panel.tsx +765 -0
  126. package/src/components/login-screen.tsx +479 -0
  127. package/src/components/session-guard.tsx +1071 -0
  128. package/src/entries/agent-chat.ts +113 -0
  129. package/src/entries/core.ts +8 -0
  130. package/src/entries/data-table.ts +41 -0
  131. package/src/entries/markdown.ts +25 -0
  132. package/src/hooks/use-cortena-theme.ts +63 -4
  133. package/src/index.ts +6 -0
@@ -0,0 +1,168 @@
1
+ "use client";
2
+ "use client";
3
+ import { Button } from "../button.js";
4
+ import { Chip } from "../chip.js";
5
+ import { EmptyState } from "../empty-state.js";
6
+ import { ErrorBanner } from "../error-banner.js";
7
+ import { SectionCard } from "../section-card.js";
8
+ import { createColumnHelper } from "../data-table/features.js";
9
+ import { DataTable } from "../data-table/data-table.js";
10
+ import { useAdminPermissions } from "./context.js";
11
+ import { DirectRoles, RoleAssignmentDialog } from "./role-assignment.js";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ import * as React from "react";
14
+ //#region src/components/admin-permissions/roles.tsx
15
+ const helper = /* @__PURE__ */ createColumnHelper();
16
+ /**
17
+ * `/admin/roles` — the extension's own vocabulary, and the group principals
18
+ * that hold it.
19
+ *
20
+ * Two halves. The first is what each declared role grants, read straight off
21
+ * the matrix, so an administrator deciding whether to hand somebody "editor"
22
+ * can see what "editor" means without leaving the screen. The second is group
23
+ * assignment: the other kind of principal a role attaches to (DESIGN-D22,
24
+ * P-42), on the same screen as the users because it is the same decision.
25
+ *
26
+ * The groups list is a **client** source, and the members list is not, for one
27
+ * reason: an org's groups are a short, bounded list that arrives in a single
28
+ * `groups.list()`, so paging it server-side would be a round-trip per keypress
29
+ * for nothing. Where the groups come from — a directory sync, or somebody
30
+ * typing them into cortena-auth — is invisible here by design.
31
+ *
32
+ * The whole section disappears when the api has no `groups`, and equally when
33
+ * it has some and the org has none.
34
+ */
35
+ function AdminRoles() {
36
+ const { matrix, hasGroups, groups, groupsState, groupRoles } = useAdminPermissions();
37
+ const [editing, setEditing] = React.useState(null);
38
+ const [editingRoles, setEditingRoles] = React.useState([]);
39
+ const columns = React.useMemo(() => helper.columns([
40
+ helper.accessor("name", {
41
+ header: "Group",
42
+ cell: ({ row }) => /* @__PURE__ */ jsx("span", {
43
+ className: "font-medium text-[color:var(--ds-foreground)]",
44
+ children: row.original.name
45
+ }),
46
+ meta: { label: "Group" }
47
+ }),
48
+ helper.accessor("memberCount", {
49
+ header: "Members",
50
+ cell: ({ row }) => row.original.memberCount ?? "—",
51
+ meta: {
52
+ align: "end",
53
+ label: "Members"
54
+ }
55
+ }),
56
+ helper.display({
57
+ id: "roles",
58
+ header: "Roles",
59
+ cell: ({ row }) => /* @__PURE__ */ jsx(DirectRoles, { roles: groupRoles[row.original.id] ?? [] }),
60
+ meta: {
61
+ label: "Roles",
62
+ exportValue: (row) => (groupRoles[row.id] ?? []).join(", ")
63
+ },
64
+ enableSorting: false
65
+ }),
66
+ helper.display({
67
+ id: "actions",
68
+ header: () => /* @__PURE__ */ jsx("span", {
69
+ className: "sr-only",
70
+ children: "Actions"
71
+ }),
72
+ cell: ({ row }) => /* @__PURE__ */ jsx("div", {
73
+ className: "flex justify-end",
74
+ children: /* @__PURE__ */ jsx(Button, {
75
+ variant: "outline",
76
+ size: "sm",
77
+ "aria-label": `Edit roles for ${row.original.name}`,
78
+ onClick: () => {
79
+ setEditingRoles(groupRoles[row.original.id] ?? []);
80
+ setEditing({
81
+ kind: "group",
82
+ id: row.original.id,
83
+ name: row.original.name
84
+ });
85
+ },
86
+ children: "Edit roles"
87
+ })
88
+ }),
89
+ meta: {
90
+ align: "end",
91
+ exportable: false
92
+ },
93
+ enableSorting: false,
94
+ enableHiding: false
95
+ })
96
+ ]), [groupRoles]);
97
+ const roles = matrix?.roles ?? [];
98
+ return /* @__PURE__ */ jsxs("div", {
99
+ "data-slot": "admin-roles",
100
+ className: "flex flex-col gap-6",
101
+ children: [
102
+ /* @__PURE__ */ jsx(SectionCard, {
103
+ title: "Roles",
104
+ description: "Declared by this extension. Nothing on the platform reads these names, so they are the extension's to choose.",
105
+ children: roles.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
106
+ size: "compact",
107
+ title: "No roles declared",
108
+ description: "Roles come from this extension's permissions document. Until it declares some, there is nothing to assign."
109
+ }) : /* @__PURE__ */ jsx("ul", {
110
+ "data-slot": "admin-role-list",
111
+ className: "flex flex-col gap-2",
112
+ children: roles.map((role) => {
113
+ const granted = matrix?.grants[role] ?? [];
114
+ return /* @__PURE__ */ jsxs("li", {
115
+ "data-slot": "admin-role-summary",
116
+ "data-role": role,
117
+ className: "flex flex-col gap-1.5 rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)] bg-[var(--ds-muted)] px-3 py-2.5",
118
+ children: [/* @__PURE__ */ jsx("span", {
119
+ className: "text-[length:var(--ds-text-body-sm)] font-semibold text-[color:var(--ds-foreground)]",
120
+ children: role
121
+ }), granted.length === 0 ? /* @__PURE__ */ jsx("span", {
122
+ className: "text-[length:var(--ds-text-caption)] text-[color:var(--ds-text-tertiary)]",
123
+ children: "Grants nothing yet"
124
+ }) : /* @__PURE__ */ jsx("div", {
125
+ className: "flex flex-wrap items-center gap-1",
126
+ children: granted.map((action) => /* @__PURE__ */ jsx(Chip, {
127
+ size: "sm",
128
+ variant: "outline",
129
+ "data-action": action,
130
+ children: action
131
+ }, action))
132
+ })]
133
+ }, role);
134
+ })
135
+ })
136
+ }),
137
+ groupsState.error ? /* @__PURE__ */ jsx(ErrorBanner, {
138
+ message: groupsState.error.message,
139
+ onRetry: groupsState.reload
140
+ }) : null,
141
+ hasGroups ? /* @__PURE__ */ jsx(SectionCard, {
142
+ title: "Groups",
143
+ description: "Groups are cortena-auth's and read-only here. Every member of a group inherits the roles assigned to it, with no assignment write of their own.",
144
+ flush: true,
145
+ children: /* @__PURE__ */ jsx(DataTable, {
146
+ columns,
147
+ dataSource: {
148
+ kind: "client",
149
+ rows: groups
150
+ },
151
+ getRowId: (row) => row.id,
152
+ searchPlaceholder: "Search groups…",
153
+ emptyMessage: "No groups",
154
+ exportFileName: "groups"
155
+ })
156
+ }) : null,
157
+ /* @__PURE__ */ jsx(RoleAssignmentDialog, {
158
+ principal: editing,
159
+ current: editingRoles,
160
+ onOpenChange: (open) => !open && setEditing(null)
161
+ })
162
+ ]
163
+ });
164
+ }
165
+ //#endregion
166
+ export { AdminRoles };
167
+
168
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","names":[],"sources":["../../../src/components/admin-permissions/roles.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/components/button\";\nimport { Chip } from \"@/components/chip\";\nimport { DataTable, createColumnHelper } from \"@/components/data-table\";\nimport { EmptyState } from \"@/components/empty-state\";\nimport { ErrorBanner } from \"@/components/error-banner\";\nimport { SectionCard } from \"@/components/section-card\";\nimport { useAdminPermissions } from \"./context\";\nimport { DirectRoles, RoleAssignmentDialog } from \"./role-assignment\";\nimport type { AdminGroupRow, AdminPrincipal } from \"./types\";\n\nconst helper = /* @__PURE__ */ createColumnHelper<AdminGroupRow>();\n\n/**\n * `/admin/roles` — the extension's own vocabulary, and the group principals\n * that hold it.\n *\n * Two halves. The first is what each declared role grants, read straight off\n * the matrix, so an administrator deciding whether to hand somebody \"editor\"\n * can see what \"editor\" means without leaving the screen. The second is group\n * assignment: the other kind of principal a role attaches to (DESIGN-D22,\n * P-42), on the same screen as the users because it is the same decision.\n *\n * The groups list is a **client** source, and the members list is not, for one\n * reason: an org's groups are a short, bounded list that arrives in a single\n * `groups.list()`, so paging it server-side would be a round-trip per keypress\n * for nothing. Where the groups come from — a directory sync, or somebody\n * typing them into cortena-auth — is invisible here by design.\n *\n * The whole section disappears when the api has no `groups`, and equally when\n * it has some and the org has none.\n */\nexport function AdminRoles() {\n const { matrix, hasGroups, groups, groupsState, groupRoles } = useAdminPermissions();\n const [editing, setEditing] = React.useState<AdminPrincipal | null>(null);\n const [editingRoles, setEditingRoles] = React.useState<readonly string[]>([]);\n\n const columns = React.useMemo(\n () =>\n helper.columns([\n helper.accessor(\"name\", {\n header: \"Group\",\n cell: ({ row }) => (\n <span className=\"font-medium text-[color:var(--ds-foreground)]\">\n {row.original.name}\n </span>\n ),\n meta: { label: \"Group\" },\n }),\n helper.accessor(\"memberCount\", {\n header: \"Members\",\n cell: ({ row }) => row.original.memberCount ?? \"—\",\n meta: { align: \"end\", label: \"Members\" },\n }),\n helper.display({\n id: \"roles\",\n header: \"Roles\",\n cell: ({ row }) => <DirectRoles roles={groupRoles[row.original.id] ?? []} />,\n meta: {\n label: \"Roles\",\n exportValue: (row: AdminGroupRow) => (groupRoles[row.id] ?? []).join(\", \"),\n },\n enableSorting: false,\n }),\n helper.display({\n id: \"actions\",\n header: () => <span className=\"sr-only\">Actions</span>,\n cell: ({ row }) => (\n <div className=\"flex justify-end\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n aria-label={`Edit roles for ${row.original.name}`}\n onClick={() => {\n setEditingRoles(groupRoles[row.original.id] ?? []);\n setEditing({ kind: \"group\", id: row.original.id, name: row.original.name });\n }}\n >\n Edit roles\n </Button>\n </div>\n ),\n meta: { align: \"end\", exportable: false },\n enableSorting: false,\n enableHiding: false,\n }),\n ]),\n [groupRoles],\n );\n\n const roles = matrix?.roles ?? [];\n\n return (\n <div data-slot=\"admin-roles\" className=\"flex flex-col gap-6\">\n <SectionCard\n title=\"Roles\"\n description=\"Declared by this extension. Nothing on the platform reads these names, so they are the extension's to choose.\"\n >\n {roles.length === 0 ? (\n <EmptyState\n size=\"compact\"\n title=\"No roles declared\"\n description=\"Roles come from this extension's permissions document. Until it declares some, there is nothing to assign.\"\n />\n ) : (\n <ul data-slot=\"admin-role-list\" className=\"flex flex-col gap-2\">\n {roles.map((role) => {\n const granted = matrix?.grants[role] ?? [];\n return (\n <li\n key={role}\n data-slot=\"admin-role-summary\"\n data-role={role}\n className=\"flex flex-col gap-1.5 rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)] bg-[var(--ds-muted)] px-3 py-2.5\"\n >\n <span className=\"text-[length:var(--ds-text-body-sm)] font-semibold text-[color:var(--ds-foreground)]\">\n {role}\n </span>\n {granted.length === 0 ? (\n <span className=\"text-[length:var(--ds-text-caption)] text-[color:var(--ds-text-tertiary)]\">\n Grants nothing yet\n </span>\n ) : (\n <div className=\"flex flex-wrap items-center gap-1\">\n {granted.map((action) => (\n <Chip key={action} size=\"sm\" variant=\"outline\" data-action={action}>\n {action}\n </Chip>\n ))}\n </div>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n\n {groupsState.error ? (\n <ErrorBanner message={groupsState.error.message} onRetry={groupsState.reload} />\n ) : null}\n\n {hasGroups ? (\n <SectionCard\n title=\"Groups\"\n description=\"Groups are cortena-auth's and read-only here. Every member of a group inherits the roles assigned to it, with no assignment write of their own.\"\n flush\n >\n <DataTable\n columns={columns}\n dataSource={{ kind: \"client\", rows: groups }}\n getRowId={(row) => row.id}\n searchPlaceholder=\"Search groups…\"\n emptyMessage=\"No groups\"\n exportFileName=\"groups\"\n />\n </SectionCard>\n ) : null}\n\n <RoleAssignmentDialog\n principal={editing}\n current={editingRoles}\n onOpenChange={(open) => !open && setEditing(null)}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,152 @@
1
+ "use client";
2
+ import { TableQuery, TableResult } from "../data-table/types.js";
3
+ import "../data-table/index.js";
4
+ //#region src/components/admin-permissions/types.d.ts
5
+ /**
6
+ * A role is assigned to a principal, never to a user id (DESIGN-D22, §13.4).
7
+ * Both kinds are edited on the same screen and through the same dialog; only
8
+ * the method underneath differs.
9
+ */
10
+ export interface AdminPrincipal {
11
+ kind: "user" | "group";
12
+ /** cortena-auth user id, or cortena-auth group id. */
13
+ id: string;
14
+ /** Display name, for the dialog title. */
15
+ name?: string;
16
+ }
17
+ /** A group, as a member's row refers to it. Defined and maintained in cortena-auth. */
18
+ export interface AdminGroupRef {
19
+ id: string;
20
+ name: string;
21
+ }
22
+ /**
23
+ * Why a member holds an effective role: assigned to them directly, or carried
24
+ * by a group they belong to. Never dropped from the rendering — an
25
+ * administrator who cannot see *why* somebody has a permission removes the
26
+ * wrong assignment and believes the job is done (P-43).
27
+ */
28
+ export type AdminRoleSource = "direct" | {
29
+ group: AdminGroupRef;
30
+ };
31
+ /** One entry of `effectiveRoles`: the role, and the assignment that granted it. */
32
+ export interface AdminEffectiveRole {
33
+ role: string;
34
+ source: AdminRoleSource;
35
+ }
36
+ /**
37
+ * A row of `members.list`. A join: the person and their groups are
38
+ * cortena-auth's and read-only; `extensionRoles` is the extension's own direct
39
+ * assignment; `effectiveRoles` is the per-request resolution of direct +
40
+ * inherited that `may()` performs, rendered — never a stored table (P-43).
41
+ */
42
+ export interface AdminMemberRow {
43
+ userId: string;
44
+ name: string;
45
+ email: string;
46
+ status: string;
47
+ /** ISO timestamp, or null when the user has never signed in. */
48
+ lastActiveAt: string | null;
49
+ groups: AdminGroupRef[];
50
+ extensionRoles: string[];
51
+ effectiveRoles: AdminEffectiveRole[];
52
+ }
53
+ /**
54
+ * A row of `groups.list` — read-only as to the group itself. The roles a group
55
+ * carries are the extension's and come back separately from `groups.roles()`,
56
+ * because the group list is identity's and the assignment map is not.
57
+ */
58
+ export interface AdminGroupRow {
59
+ id: string;
60
+ name: string;
61
+ memberCount: number;
62
+ }
63
+ /** `licence.get` — a read-through display of cortena-auth's licence. */
64
+ export interface AdminLicence {
65
+ plan: string;
66
+ seats: number | null;
67
+ seatsUsed: number | null;
68
+ features: string[];
69
+ }
70
+ /**
71
+ * `permissions.get` / `permissions.put` — the extension's own matrix, in the
72
+ * extension's own database. This is the source of truth that both the REST
73
+ * routes and the MCP tools enforce through one `may()` (§13.2, P-14).
74
+ */
75
+ export interface AdminPermissionMatrix {
76
+ roles: string[];
77
+ actions: string[];
78
+ /** role → the actions it grants. A role absent from the map grants nothing. */
79
+ grants: Record<string, string[]>;
80
+ }
81
+ /**
82
+ * Everything the composition talks to, one namespace per resource in §13.3.
83
+ *
84
+ * `groups` is the only optional member, and it is optional for a reason rather
85
+ * than for convenience: an extension whose cortena-auth tenant has no groups
86
+ * omits it, and every group affordance disappears instead of rendering empty.
87
+ * It disappears equally when `groups.list()` comes back empty — an empty
88
+ * "Groups" section reads as a broken feature, and there is nothing an
89
+ * administrator can do about it from here.
90
+ */
91
+ export interface AdminPermissionsApi {
92
+ members: {
93
+ /** `GET .../admin/members` — the DataTable server source. */
94
+ list: (query: TableQuery, signal?: AbortSignal) => Promise<TableResult<AdminMemberRow>>;
95
+ /** `PATCH .../admin/members/:userId { extensionRoles }` — direct assignment only. */
96
+ setRoles: (userId: string, roles: string[]) => Promise<void>;
97
+ };
98
+ groups?: {
99
+ /** `GET .../admin/groups` — read-only; the whole list, small enough to be a client source. */
100
+ list: (signal?: AbortSignal) => Promise<AdminGroupRow[]>;
101
+ /** The extension's assignment: group id → the roles that group carries. */
102
+ roles: (signal?: AbortSignal) => Promise<Record<string, string[]>>;
103
+ /** `PATCH .../admin/groups/:groupId { extensionRoles }` */
104
+ setRoles: (groupId: string, roles: string[]) => Promise<void>;
105
+ };
106
+ licence: {
107
+ /** `GET .../admin/licence` — read-only, always. */
108
+ get: (signal?: AbortSignal) => Promise<AdminLicence>;
109
+ };
110
+ permissions: {
111
+ /** `GET .../admin/permissions` */
112
+ get: (signal?: AbortSignal) => Promise<AdminPermissionMatrix>;
113
+ /** `PUT .../admin/permissions` */
114
+ put: (matrix: AdminPermissionMatrix, signal?: AbortSignal) => Promise<AdminPermissionMatrix | void>;
115
+ };
116
+ }
117
+ /** The four screens, named after their paths under `admin/`. */
118
+ export type AdminRoute = "members" | "roles" | "licence" | "permissions";
119
+ export declare const ADMIN_ROUTES: readonly AdminRoute[];
120
+ export declare const ADMIN_ROUTE_LABELS: Record<AdminRoute, string>;
121
+ /**
122
+ * The broker's error envelope (§15.6). A failed mutation must show what the
123
+ * server said — "assigneeId: Required" and "Invalid input" cost the same to
124
+ * produce and differ by a whole round-trip — so the rejection is unwrapped
125
+ * rather than replaced with a house message.
126
+ */
127
+ export interface AdminErrorEnvelope {
128
+ ok: false;
129
+ status?: number;
130
+ code?: string;
131
+ message: string;
132
+ details?: unknown;
133
+ }
134
+ /**
135
+ * The message to show for a rejection.
136
+ *
137
+ * A fetch wrapper usually rejects with an `Error` and hangs the parsed
138
+ * envelope off it, so the envelope is preferred over the generic
139
+ * `"HTTP 403"` the Error itself carries. A rejection that *is* the envelope,
140
+ * and a plain `Error`, both work as well.
141
+ */
142
+ export declare function adminErrorMessage(error: unknown): string;
143
+ /**
144
+ * The effective roles to render. When the server sends none, the direct
145
+ * assignments are shown as direct — which is what they are — rather than the
146
+ * screen inventing an inheritance it cannot know about.
147
+ */
148
+ export declare function effectiveRolesOf(row: AdminMemberRow): AdminEffectiveRole[];
149
+ /** The group behind an effective role, or `null` for a direct assignment. */
150
+ export declare function sourceGroupOf(source: AdminRoleSource): AdminGroupRef | null;
151
+ //#endregion
152
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ //#region src/components/admin-permissions/types.ts
3
+ const ADMIN_ROUTES = [
4
+ "members",
5
+ "roles",
6
+ "licence",
7
+ "permissions"
8
+ ];
9
+ const ADMIN_ROUTE_LABELS = {
10
+ members: "Members",
11
+ roles: "Roles",
12
+ licence: "Licence",
13
+ permissions: "Permissions"
14
+ };
15
+ function envelopeMessage(value) {
16
+ if (!value || typeof value !== "object") return null;
17
+ const message = value.message;
18
+ return typeof message === "string" && message.length > 0 ? message : null;
19
+ }
20
+ /**
21
+ * The message to show for a rejection.
22
+ *
23
+ * A fetch wrapper usually rejects with an `Error` and hangs the parsed
24
+ * envelope off it, so the envelope is preferred over the generic
25
+ * `"HTTP 403"` the Error itself carries. A rejection that *is* the envelope,
26
+ * and a plain `Error`, both work as well.
27
+ */
28
+ function adminErrorMessage(error) {
29
+ if (error && typeof error === "object") {
30
+ const carrier = error;
31
+ const nested = envelopeMessage(carrier.envelope) ?? envelopeMessage(carrier.body) ?? envelopeMessage(carrier.error);
32
+ if (nested) return nested;
33
+ }
34
+ if (error instanceof Error) return error.message || "Something went wrong";
35
+ const direct = envelopeMessage(error);
36
+ if (direct) return direct;
37
+ return String(error);
38
+ }
39
+ /** Tolerates a server that omits an array the contract says is required. */
40
+ function groupsOf(row) {
41
+ return row.groups ?? [];
42
+ }
43
+ /**
44
+ * The effective roles to render. When the server sends none, the direct
45
+ * assignments are shown as direct — which is what they are — rather than the
46
+ * screen inventing an inheritance it cannot know about.
47
+ */
48
+ function effectiveRolesOf(row) {
49
+ if (row.effectiveRoles?.length) return row.effectiveRoles;
50
+ return (row.extensionRoles ?? []).map((role) => ({
51
+ role,
52
+ source: "direct"
53
+ }));
54
+ }
55
+ /** The group behind an effective role, or `null` for a direct assignment. */
56
+ function sourceGroupOf(source) {
57
+ if (source === "direct" || !source) return null;
58
+ return source.group ?? null;
59
+ }
60
+ //#endregion
61
+ export { ADMIN_ROUTES, ADMIN_ROUTE_LABELS, adminErrorMessage, effectiveRolesOf, groupsOf, sourceGroupOf };
62
+
63
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/components/admin-permissions/types.ts"],"sourcesContent":["/**\n * The contract AdminPermissions renders, from §13.3 of\n * \"How to create a Cortena extension\" (DESIGN-D11, amended by DESIGN-D22).\n *\n * The composition ships **no vocabulary of its own**. Every role, every action\n * and every grant on screen arrives through the `api` prop; every principal\n * arrives through it too. That is what lets two extensions with completely\n * different role models look like one product — and it is why nothing in this\n * folder has a default role list, a default action list, or an opinion about\n * what \"admin\" means.\n *\n * The split the screen is built around:\n *\n * | cortena-auth owns | the extension owns |\n * | --- | --- |\n * | who the user is, their org membership and status | every role it recognises |\n * | the groups, their names and their membership | the roles × actions matrix |\n * | the licence | which principal holds which role |\n *\n * So the person half of the members list is read-only here, the group half is\n * read-only too, and the roles beside them are the only editable thing.\n *\n * Where the groups come from is deliberately invisible: an extension asks\n * `api.groups` for `{ id, name, memberCount }` and never learns whether that\n * is a directory sync, an SCIM push or a hand-made list in cortena-auth.\n */\n\nimport type { TableQuery, TableResult } from \"@/components/data-table\";\n\n/**\n * A role is assigned to a principal, never to a user id (DESIGN-D22, §13.4).\n * Both kinds are edited on the same screen and through the same dialog; only\n * the method underneath differs.\n */\nexport interface AdminPrincipal {\n kind: \"user\" | \"group\";\n /** cortena-auth user id, or cortena-auth group id. */\n id: string;\n /** Display name, for the dialog title. */\n name?: string;\n}\n\n/** A group, as a member's row refers to it. Defined and maintained in cortena-auth. */\nexport interface AdminGroupRef {\n id: string;\n name: string;\n}\n\n/**\n * Why a member holds an effective role: assigned to them directly, or carried\n * by a group they belong to. Never dropped from the rendering — an\n * administrator who cannot see *why* somebody has a permission removes the\n * wrong assignment and believes the job is done (P-43).\n */\nexport type AdminRoleSource = \"direct\" | { group: AdminGroupRef };\n\n/** One entry of `effectiveRoles`: the role, and the assignment that granted it. */\nexport interface AdminEffectiveRole {\n role: string;\n source: AdminRoleSource;\n}\n\n/**\n * A row of `members.list`. A join: the person and their groups are\n * cortena-auth's and read-only; `extensionRoles` is the extension's own direct\n * assignment; `effectiveRoles` is the per-request resolution of direct +\n * inherited that `may()` performs, rendered — never a stored table (P-43).\n */\nexport interface AdminMemberRow {\n userId: string;\n name: string;\n email: string;\n status: string;\n /** ISO timestamp, or null when the user has never signed in. */\n lastActiveAt: string | null;\n groups: AdminGroupRef[];\n extensionRoles: string[];\n effectiveRoles: AdminEffectiveRole[];\n}\n\n/**\n * A row of `groups.list` — read-only as to the group itself. The roles a group\n * carries are the extension's and come back separately from `groups.roles()`,\n * because the group list is identity's and the assignment map is not.\n */\nexport interface AdminGroupRow {\n id: string;\n name: string;\n memberCount: number;\n}\n\n/** `licence.get` — a read-through display of cortena-auth's licence. */\nexport interface AdminLicence {\n plan: string;\n seats: number | null;\n seatsUsed: number | null;\n features: string[];\n}\n\n/**\n * `permissions.get` / `permissions.put` — the extension's own matrix, in the\n * extension's own database. This is the source of truth that both the REST\n * routes and the MCP tools enforce through one `may()` (§13.2, P-14).\n */\nexport interface AdminPermissionMatrix {\n roles: string[];\n actions: string[];\n /** role → the actions it grants. A role absent from the map grants nothing. */\n grants: Record<string, string[]>;\n}\n\n/**\n * Everything the composition talks to, one namespace per resource in §13.3.\n *\n * `groups` is the only optional member, and it is optional for a reason rather\n * than for convenience: an extension whose cortena-auth tenant has no groups\n * omits it, and every group affordance disappears instead of rendering empty.\n * It disappears equally when `groups.list()` comes back empty — an empty\n * \"Groups\" section reads as a broken feature, and there is nothing an\n * administrator can do about it from here.\n */\nexport interface AdminPermissionsApi {\n members: {\n /** `GET .../admin/members` — the DataTable server source. */\n list: (query: TableQuery, signal?: AbortSignal) => Promise<TableResult<AdminMemberRow>>;\n /** `PATCH .../admin/members/:userId { extensionRoles }` — direct assignment only. */\n setRoles: (userId: string, roles: string[]) => Promise<void>;\n };\n groups?: {\n /** `GET .../admin/groups` — read-only; the whole list, small enough to be a client source. */\n list: (signal?: AbortSignal) => Promise<AdminGroupRow[]>;\n /** The extension's assignment: group id → the roles that group carries. */\n roles: (signal?: AbortSignal) => Promise<Record<string, string[]>>;\n /** `PATCH .../admin/groups/:groupId { extensionRoles }` */\n setRoles: (groupId: string, roles: string[]) => Promise<void>;\n };\n licence: {\n /** `GET .../admin/licence` — read-only, always. */\n get: (signal?: AbortSignal) => Promise<AdminLicence>;\n };\n permissions: {\n /** `GET .../admin/permissions` */\n get: (signal?: AbortSignal) => Promise<AdminPermissionMatrix>;\n /** `PUT .../admin/permissions` */\n put: (\n matrix: AdminPermissionMatrix,\n signal?: AbortSignal,\n ) => Promise<AdminPermissionMatrix | void>;\n };\n}\n\n/** The four screens, named after their paths under `admin/`. */\nexport type AdminRoute = \"members\" | \"roles\" | \"licence\" | \"permissions\";\n\nexport const ADMIN_ROUTES: readonly AdminRoute[] = [\n \"members\",\n \"roles\",\n \"licence\",\n \"permissions\",\n];\n\nexport const ADMIN_ROUTE_LABELS: Record<AdminRoute, string> = {\n members: \"Members\",\n roles: \"Roles\",\n licence: \"Licence\",\n permissions: \"Permissions\",\n};\n\n/**\n * The broker's error envelope (§15.6). A failed mutation must show what the\n * server said — \"assigneeId: Required\" and \"Invalid input\" cost the same to\n * produce and differ by a whole round-trip — so the rejection is unwrapped\n * rather than replaced with a house message.\n */\nexport interface AdminErrorEnvelope {\n ok: false;\n status?: number;\n code?: string;\n message: string;\n details?: unknown;\n}\n\nfunction envelopeMessage(value: unknown): string | null {\n if (!value || typeof value !== \"object\") return null;\n const message = (value as { message?: unknown }).message;\n return typeof message === \"string\" && message.length > 0 ? message : null;\n}\n\n/**\n * The message to show for a rejection.\n *\n * A fetch wrapper usually rejects with an `Error` and hangs the parsed\n * envelope off it, so the envelope is preferred over the generic\n * `\"HTTP 403\"` the Error itself carries. A rejection that *is* the envelope,\n * and a plain `Error`, both work as well.\n */\nexport function adminErrorMessage(error: unknown): string {\n if (error && typeof error === \"object\") {\n const carrier = error as { envelope?: unknown; body?: unknown; error?: unknown };\n const nested =\n envelopeMessage(carrier.envelope) ??\n envelopeMessage(carrier.body) ??\n envelopeMessage(carrier.error);\n if (nested) return nested;\n }\n if (error instanceof Error) return error.message || \"Something went wrong\";\n const direct = envelopeMessage(error);\n if (direct) return direct;\n return String(error);\n}\n\n/** Tolerates a server that omits an array the contract says is required. */\nexport function groupsOf(row: AdminMemberRow): AdminGroupRef[] {\n return row.groups ?? [];\n}\n\n/**\n * The effective roles to render. When the server sends none, the direct\n * assignments are shown as direct — which is what they are — rather than the\n * screen inventing an inheritance it cannot know about.\n */\nexport function effectiveRolesOf(row: AdminMemberRow): AdminEffectiveRole[] {\n if (row.effectiveRoles?.length) return row.effectiveRoles;\n return (row.extensionRoles ?? []).map((role) => ({ role, source: \"direct\" as const }));\n}\n\n/** The group behind an effective role, or `null` for a direct assignment. */\nexport function sourceGroupOf(source: AdminRoleSource): AdminGroupRef | null {\n if (source === \"direct\" || !source) return null;\n return source.group ?? null;\n}\n"],"mappings":";;AA0JA,MAAa,eAAsC;CACjD;CACA;CACA;CACA;AACF;AAEA,MAAa,qBAAiD;CAC5D,SAAS;CACT,OAAO;CACP,SAAS;CACT,aAAa;AACf;AAgBA,SAAS,gBAAgB,OAA+B;CACtD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAChD,MAAM,UAAW,MAAgC;CACjD,OAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;AACvE;;;;;;;;;AAUA,SAAgB,kBAAkB,OAAwB;CACxD,IAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,UAAU;EAChB,MAAM,SACJ,gBAAgB,QAAQ,QAAQ,KAChC,gBAAgB,QAAQ,IAAI,KAC5B,gBAAgB,QAAQ,KAAK;EAC/B,IAAI,QAAQ,OAAO;CACrB;CACA,IAAI,iBAAiB,OAAO,OAAO,MAAM,WAAW;CACpD,MAAM,SAAS,gBAAgB,KAAK;CACpC,IAAI,QAAQ,OAAO;CACnB,OAAO,OAAO,KAAK;AACrB;;AAGA,SAAgB,SAAS,KAAsC;CAC7D,OAAO,IAAI,UAAU,CAAC;AACxB;;;;;;AAOA,SAAgB,iBAAiB,KAA2C;CAC1E,IAAI,IAAI,gBAAgB,QAAQ,OAAO,IAAI;CAC3C,QAAQ,IAAI,kBAAkB,CAAC,EAAA,CAAG,KAAK,UAAU;EAAE;EAAM,QAAQ;CAAkB,EAAE;AACvF;;AAGA,SAAgB,cAAc,QAA+C;CAC3E,IAAI,WAAW,YAAY,CAAC,QAAQ,OAAO;CAC3C,OAAO,OAAO,SAAS;AACzB"}
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { AgentChatClient } from "../agent-chat/types.js";
3
+ import { AgentChatProps } from "./agent-chat.js";
4
+ import * as React from "react";
5
+ //#region src/components/agent-chat-popup.d.ts
6
+ export type AgentChatPopupState = "pill" | "panel" | "full";
7
+ export interface AgentChatPopupExtension {
8
+ /** Registered mark id — the same `x-cortena.icon` the shell draws. */
9
+ id: string;
10
+ /** Display name. Also the agent template slug, unless `agentId` overrides it. */
11
+ name: string;
12
+ }
13
+ export interface AgentChatPopupProps extends Omit<AgentChatProps, "agentName" | "storageScope" | "className" | "headerSlot" | "client"> {
14
+ extension: AgentChatPopupExtension;
15
+ /** The transport. Its `agentId` is the template slug the sessions belong to. */
16
+ client: AgentChatClient;
17
+ /**
18
+ * Names the `sessionStorage` slots. Defaults to the extension id, which is
19
+ * what keeps two extensions on one origin out of each other's sessions.
20
+ */
21
+ storageScope?: string;
22
+ /** Start here instead of collapsed; the stored state still wins on reload. */
23
+ defaultState?: AgentChatPopupState;
24
+ className?: string;
25
+ }
26
+ declare function AgentChatPopup({ extension, client, storageScope, defaultState, className, ...chatProps }: AgentChatPopupProps): React.JSX.Element;
27
+ //#endregion
28
+ export { AgentChatPopup };
29
+ //# sourceMappingURL=agent-chat-popup.d.ts.map
@@ -0,0 +1,188 @@
1
+ "use client";
2
+ "use client";
3
+ import { cn } from "../lib/cn.js";
4
+ import { BrandMark } from "./app-shell.js";
5
+ import { Button } from "./button.js";
6
+ import { viewStorageKey, windowSessionStorage } from "../agent-chat/session.js";
7
+ import { AgentChat } from "./agent-chat.js";
8
+ import { Maximize2, Minimize2, X } from "lucide-react";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import * as React from "react";
11
+ //#region src/components/agent-chat-popup.tsx
12
+ /**
13
+ * AgentChatPopup — the extension's own agent, in the corner of every screen.
14
+ *
15
+ * §17 of how-to-create-a-cortena-extension, audit rule P-25. Mounted in
16
+ * `AppShell`'s `agentSlot`, so it stacks above the theme/help cluster rather
17
+ * than beside it and no extension positions a floating control itself.
18
+ *
19
+ * ```tsx
20
+ * <AppShell
21
+ * extension={{ id: "tasks", name: "Tasks" }}
22
+ * user={user}
23
+ * agentSlot={
24
+ * <AgentChatPopup
25
+ * extension={{ id: "tasks", name: "Tasks" }}
26
+ * client={client} // createAguiAgentChatClient({ …, agentId: "tasks" })
27
+ * />
28
+ * }
29
+ * />
30
+ * ```
31
+ *
32
+ * Three states, and the transitions between them are fixed: a collapsed pill,
33
+ * a panel, and full screen. **Cmd/Ctrl+J** toggles between the pill and the
34
+ * panel; **Escape** steps full screen -> panel -> pill, except while a run is
35
+ * streaming, where the composer takes Escape to mean "stop" — the first Escape
36
+ * during a run is nearly always about the run, not about the window.
37
+ *
38
+ * The chat stays mounted in every state, hidden rather than unmounted while
39
+ * collapsed. That is not an optimisation: a run started before the user
40
+ * collapsed the pop-up keeps streaming into it, and the unread dot on the pill
41
+ * is what says so. Unmounting would throw the reply away halfway through.
42
+ *
43
+ * State is remembered per window, in `sessionStorage`, alongside the session
44
+ * key — a second window of the same extension opens its own chat on its own
45
+ * session rather than inheriting this one's.
46
+ */
47
+ /** Cmd on a Mac, Ctrl elsewhere. Documented in README.md. */
48
+ function isToggleShortcut(event) {
49
+ return (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === "j";
50
+ }
51
+ function AgentChatPopup({ extension, client, storageScope, defaultState = "pill", className, ...chatProps }) {
52
+ const scope = storageScope ?? extension.id;
53
+ const [state, setState] = React.useState(defaultState);
54
+ const [unread, setUnread] = React.useState(false);
55
+ const stateRef = React.useRef(state);
56
+ stateRef.current = state;
57
+ const pillRef = React.useRef(null);
58
+ const panelRef = React.useRef(null);
59
+ /** Whether the user has opened or closed it, as against it starting open. */
60
+ const moved = React.useRef(false);
61
+ React.useEffect(() => {
62
+ try {
63
+ const stored = windowSessionStorage()?.getItem(viewStorageKey(scope));
64
+ if (stored === "pill" || stored === "panel" || stored === "full") setState(stored);
65
+ } catch {}
66
+ }, [scope]);
67
+ const go = React.useCallback((next) => {
68
+ moved.current = true;
69
+ setState(next);
70
+ if (next !== "pill") setUnread(false);
71
+ try {
72
+ windowSessionStorage()?.setItem(viewStorageKey(scope), next);
73
+ } catch {}
74
+ }, [scope]);
75
+ React.useEffect(() => {
76
+ return client.onChat((event) => {
77
+ if (stateRef.current === "pill" && (event.state === "delta" || event.state === "final")) setUnread(true);
78
+ });
79
+ }, [client]);
80
+ React.useEffect(() => {
81
+ const onKeyDown = (event) => {
82
+ if (!isToggleShortcut(event)) return;
83
+ event.preventDefault();
84
+ go(stateRef.current === "pill" ? "panel" : "pill");
85
+ };
86
+ window.addEventListener("keydown", onKeyDown);
87
+ return () => window.removeEventListener("keydown", onKeyDown);
88
+ }, [go]);
89
+ /**
90
+ * Escape steps down one level. It is handled on the panel rather than on the
91
+ * window so it only fires when focus is inside the pop-up, and it arrives
92
+ * here only when the composer did not claim it — the composer stops
93
+ * propagation while a run is streaming, where Escape means "stop the run".
94
+ */
95
+ const onKeyDown = React.useCallback((event) => {
96
+ if (event.key !== "Escape") return;
97
+ event.preventDefault();
98
+ go(stateRef.current === "full" ? "panel" : "pill");
99
+ }, [go]);
100
+ const open = state !== "pill";
101
+ /**
102
+ * Focus follows the pop-up.
103
+ *
104
+ * The panel is `hidden`, not unmounted, so nothing moves focus by itself.
105
+ * Opening left focus on the pill — which is now `hidden`, so the browser
106
+ * resets it to <body> and the next Tab starts at the top of the page rather
107
+ * than in the chat that was just opened. Closing had the mirror problem:
108
+ * focus was inside a subtree that had just become hidden.
109
+ *
110
+ * Only after the user moves it. A pop-up that starts on `panel`, or one
111
+ * restored from `sessionStorage` on load, must not steal focus from the page.
112
+ */
113
+ React.useEffect(() => {
114
+ if (!moved.current) return;
115
+ const frame = requestAnimationFrame(() => {
116
+ if (open) panelRef.current?.querySelector("[data-slot=\"agent-chat-input\"]")?.focus({ preventScroll: true });
117
+ else pillRef.current?.focus({ preventScroll: true });
118
+ });
119
+ return () => cancelAnimationFrame(frame);
120
+ }, [open]);
121
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
122
+ ref: pillRef,
123
+ type: "button",
124
+ "data-slot": "agent-chat-pill",
125
+ "data-state": state,
126
+ "data-unread": unread ? "" : void 0,
127
+ "aria-expanded": open,
128
+ "aria-label": `Ask ${extension.name}`,
129
+ hidden: open,
130
+ onClick: () => go("panel"),
131
+ className: cn("relative inline-flex h-10 items-center gap-2 rounded-[var(--ds-radius-full)] px-3.5", "border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]", "shadow-[var(--ds-shadow-md)] outline-none", "transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]", "hover:bg-[var(--ds-hover)]", "focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2", "focus-visible:ring-offset-[var(--ds-background)]", className),
132
+ children: [
133
+ /* @__PURE__ */ jsx("span", {
134
+ className: "flex size-5 items-center justify-center text-[color:var(--ds-primary)]",
135
+ children: /* @__PURE__ */ jsx(BrandMark, {
136
+ id: extension.id,
137
+ name: extension.name,
138
+ size: 16,
139
+ decorative: true
140
+ })
141
+ }),
142
+ /* @__PURE__ */ jsxs("span", {
143
+ className: "text-[length:var(--ds-text-caption-lg)] font-medium",
144
+ children: ["Ask ", extension.name]
145
+ }),
146
+ unread ? /* @__PURE__ */ jsx("span", {
147
+ "data-slot": "agent-chat-pill-unread",
148
+ "aria-label": "The agent is replying",
149
+ className: "size-2 rounded-[var(--ds-radius-full)] bg-[var(--ds-primary)]"
150
+ }) : null
151
+ ]
152
+ }), /* @__PURE__ */ jsx("div", {
153
+ ref: panelRef,
154
+ "data-slot": "agent-chat-panel",
155
+ "data-state": state,
156
+ role: "dialog",
157
+ "aria-label": `${extension.name} agent`,
158
+ hidden: !open,
159
+ onKeyDown,
160
+ className: cn("fixed z-50 flex flex-col overflow-hidden border border-[var(--ds-border)]", "bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]", state === "full" ? "inset-0 rounded-none" : cn("right-6 bottom-6 rounded-[var(--ds-radius-lg)]", "h-[min(40rem,calc(100vh-3rem))] w-[min(26.25rem,calc(100vw-3rem))]")),
161
+ children: /* @__PURE__ */ jsx(AgentChat, {
162
+ client,
163
+ agentName: extension.name,
164
+ storageScope: scope,
165
+ className: "min-h-0 flex-1",
166
+ headerSlot: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
167
+ size: "icon-sm",
168
+ variant: "ghost",
169
+ "data-slot": "agent-chat-expand",
170
+ "aria-label": state === "full" ? "Restore the panel" : "Expand to full screen",
171
+ onClick: () => go(state === "full" ? "panel" : "full"),
172
+ children: state === "full" ? /* @__PURE__ */ jsx(Minimize2, { "aria-hidden": true }) : /* @__PURE__ */ jsx(Maximize2, { "aria-hidden": true })
173
+ }), /* @__PURE__ */ jsx(Button, {
174
+ size: "icon-sm",
175
+ variant: "ghost",
176
+ "data-slot": "agent-chat-collapse",
177
+ "aria-label": "Collapse the agent",
178
+ onClick: () => go("pill"),
179
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": true })
180
+ })] }),
181
+ ...chatProps
182
+ })
183
+ })] });
184
+ }
185
+ //#endregion
186
+ export { AgentChatPopup };
187
+
188
+ //# sourceMappingURL=agent-chat-popup.js.map