portal-mojo 0.1.0 → 0.1.1
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 +42 -1
- package/docs/admin-assistant.md +14 -7
- package/docs/image-editor.md +13 -0
- package/docs/menus-and-access.md +14 -0
- package/docs/realtime.md +4 -2
- package/package.json +14 -2
- package/src/admin/assistant/AssistantLauncher.tsx +23 -0
- package/src/admin/assistant/AssistantPanel.tsx +3 -2
- package/src/admin/assistant/api.ts +1 -1
- package/src/admin/assistant/index.ts +1 -11
- package/src/admin/assistant/launchers.tsx +5 -11
- package/src/admin/assistant/pages.tsx +1 -1
- package/src/admin/assistant/permissions.ts +1 -0
- package/src/admin/assistant/streaming.ts +69 -41
- package/src/admin/bouncer/index.ts +1 -28
- package/src/admin/bouncer/models.ts +1 -1
- package/src/admin/bouncer/signatures.tsx +1 -1
- package/src/admin/cloudwatch/CloudWatchChart.tsx +1 -1
- package/src/admin/cloudwatch/data.ts +1 -1
- package/src/admin/cloudwatch/index.ts +1 -11
- package/src/admin/core/index.ts +215 -0
- package/src/admin/credentials/group-api-keys.tsx +1 -1
- package/src/admin/credentials/models.ts +1 -1
- package/src/admin/credentials/webhook-subscriptions.tsx +1 -1
- package/src/admin/dashboard/AdminDashboardPage.tsx +9 -18
- package/src/admin/dashboard/data.ts +1 -1
- package/src/admin/dashboard/index.ts +1 -0
- package/src/admin/dns/CertificateDetail.tsx +1 -1
- package/src/admin/dns/CertificateLifecyclePoller.tsx +1 -1
- package/src/admin/dns/CertificatesPage.tsx +1 -1
- package/src/admin/dns/DnsRecordsPanel.tsx +1 -1
- package/src/admin/dns/DomainCertificatesSection.tsx +1 -1
- package/src/admin/dns/DomainDetail.tsx +1 -1
- package/src/admin/dns/DomainOnboardingDialog.tsx +1 -1
- package/src/admin/dns/DomainPurchasesPage.tsx +1 -1
- package/src/admin/dns/ProviderCredentialsPage.tsx +1 -1
- package/src/admin/dns/RegistrantContactPage.tsx +1 -1
- package/src/admin/dns/api.ts +1 -1
- package/src/admin/dns/index.ts +2 -59
- package/src/admin/dns/models.ts +1 -1
- package/src/admin/dns/setup.ts +27 -0
- package/src/admin/domains/assistant.ts +13 -0
- package/src/admin/domains/communications.ts +35 -0
- package/src/admin/domains/identity.ts +24 -0
- package/src/admin/domains/infrastructure.ts +24 -0
- package/src/admin/domains/observability.ts +28 -0
- package/src/admin/domains/operations.ts +18 -0
- package/src/admin/domains/security.ts +55 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +1 -1
- package/src/admin/identity/members/MemberDetail.tsx +1 -1
- package/src/admin/identity/members/MembersPage.tsx +1 -1
- package/src/admin/identity/members/index.ts +1 -17
- package/src/admin/identity/members/member-flows.tsx +1 -1
- package/src/admin/identity/members/models.ts +1 -1
- package/src/admin/identity/users/UserDetail.tsx +1 -1
- package/src/admin/identity/users/UsersPage.tsx +1 -1
- package/src/admin/identity/users/index.ts +1 -16
- package/src/admin/identity/users/models.ts +1 -1
- package/src/admin/identity/users/sections/LoginsSection.tsx +1 -1
- package/src/admin/identity/users/sections/NotificationsSection.tsx +1 -1
- package/src/admin/identity/users/sections/PersonalSection.tsx +1 -1
- package/src/admin/identity/users/sections/actions.tsx +1 -1
- package/src/admin/identity/users/sections/permission-catalog.ts +1 -1
- package/src/admin/identity/users/sections/queries.ts +1 -1
- package/src/admin/identity/users/sections/shared.tsx +1 -1
- package/src/admin/incidents/EventsPage.tsx +1 -1
- package/src/admin/incidents/IncidentDetail.tsx +1 -1
- package/src/admin/incidents/IncidentsPage.tsx +1 -1
- package/src/admin/incidents/index.ts +1 -8
- package/src/admin/incidents/models.ts +1 -1
- package/src/admin/index.ts +5 -364
- package/src/admin/jobs/JobDetail.tsx +1 -1
- package/src/admin/jobs/JobsTablePage.tsx +1 -1
- package/src/admin/jobs/RunnerDetail.tsx +2 -2
- package/src/admin/jobs/ScheduledTaskDetail.tsx +1 -1
- package/src/admin/jobs/ScheduledTasksPage.tsx +1 -1
- package/src/admin/jobs/columns.tsx +1 -1
- package/src/admin/jobs/control.ts +1 -1
- package/src/admin/jobs/index.ts +1 -39
- package/src/admin/jobs/models.ts +1 -1
- package/src/admin/jobs/queries.ts +1 -1
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +1 -1
- package/src/admin/jobs/sections/JobOperationsSection.tsx +1 -1
- package/src/admin/messaging/EmailDomainWizard.tsx +1 -1
- package/src/admin/messaging/PublicMessagesPage.tsx +1 -1
- package/src/admin/messaging/api.ts +1 -1
- package/src/admin/messaging/data.ts +1 -1
- package/src/admin/messaging/index.ts +1 -9
- package/src/admin/messaging/models.ts +1 -1
- package/src/admin/messaging/push/PushPage.tsx +1 -1
- package/src/admin/messaging/push/api.ts +1 -1
- package/src/admin/messaging/push/index.ts +1 -4
- package/src/admin/messaging/push/models.ts +1 -1
- package/src/admin/monitoring/MetricsExplorerPage.tsx +1 -1
- package/src/admin/monitoring/MetricsSourcePicker.tsx +1 -1
- package/src/admin/monitoring/index.ts +1 -41
- package/src/admin/monitoring/metrics-explorer-client.ts +2 -2
- package/src/admin/monitoring/models.ts +1 -1
- package/src/admin/network/BlockedIPsPage.tsx +1 -1
- package/src/admin/network/BlockedIpDetail.tsx +1 -1
- package/src/admin/network/IPSetDetail.tsx +1 -1
- package/src/admin/network/IPSetsPage.tsx +1 -1
- package/src/admin/network/geofence/BlocksTab.tsx +1 -1
- package/src/admin/network/geofence/ExemptionsTab.tsx +1 -1
- package/src/admin/network/geofence/GeofencingPage.tsx +1 -1
- package/src/admin/network/geofence/RulesTab.tsx +1 -1
- package/src/admin/network/geofence/geofence-data.ts +3 -21
- package/src/admin/network/geofence/permissions.ts +13 -0
- package/src/admin/network/index.ts +1 -41
- package/src/admin/network/models.ts +3 -9
- package/src/admin/network/permissions.ts +8 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +3 -3
- package/src/admin/phonehub/api.ts +1 -1
- package/src/admin/phonehub/data.ts +1 -1
- package/src/admin/phonehub/index.ts +1 -4
- package/src/admin/phonehub/models.ts +1 -1
- package/src/admin/public/assistant-launcher.ts +1 -0
- package/src/admin/public/assistant.ts +2 -0
- package/src/admin/public/communications.ts +4 -0
- package/src/admin/public/identity.ts +3 -0
- package/src/admin/public/infrastructure.ts +4 -0
- package/src/admin/public/observability.ts +4 -0
- package/src/admin/public/operations.ts +3 -0
- package/src/admin/public/security.ts +7 -0
- package/src/admin/registry.ts +40 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +1 -1
- package/src/admin/rules/RuleSetsPage.tsx +1 -1
- package/src/admin/rules/index.ts +1 -6
- package/src/admin/rules/models.ts +1 -1
- package/src/admin/security/devices/LoginEventDetail.tsx +1 -1
- package/src/admin/security/devices/LoginLocationsPage.tsx +1 -1
- package/src/admin/security/devices/UserDeviceDetail.tsx +1 -1
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +1 -1
- package/src/admin/security/devices/index.ts +1 -22
- package/src/admin/security/devices/models.ts +1 -1
- package/src/admin/security/geoip/GeoIpCachePage.tsx +1 -1
- package/src/admin/security/geoip/GeoIpDossier.tsx +1 -1
- package/src/admin/security/geoip/geoip-forms.ts +1 -1
- package/src/admin/security/geoip/index.ts +1 -17
- package/src/admin/security/geoip/models.ts +1 -1
- package/src/admin/security/index.ts +1 -23
- package/src/admin/security/models.ts +1 -1
- package/src/admin/security/tickets.tsx +1 -1
- package/src/admin/settings/SettingDetail.tsx +1 -1
- package/src/admin/settings/SettingEditor.tsx +1 -1
- package/src/admin/settings/index.ts +1 -13
- package/src/admin/settings/model.ts +1 -1
- package/src/admin/shortlinks/index.ts +1 -12
- package/src/admin/shortlinks/models.ts +1 -1
- package/src/admin/storage/BackendsPage.tsx +1 -1
- package/src/admin/storage/FileManagerDetail.tsx +1 -1
- package/src/admin/storage/FileView.tsx +1 -1
- package/src/admin/storage/FilesPage.tsx +1 -1
- package/src/admin/storage/index.ts +1 -18
- package/src/admin/storage/models.ts +3 -4
- package/src/admin/storage/permissions.ts +3 -0
- package/src/admin/storage/storage-dialogs.tsx +1 -1
- package/src/client/client.ts +7 -1
- package/src/client/mock.ts +10 -11
- package/src/client/realtime-mock.ts +17 -13
- package/src/client/realtime.ts +5 -4
- package/src/client/runtime.ts +21 -0
- package/src/client/upload.ts +6 -3
- package/src/ui/image-editor/ImageEditor.tsx +21 -22
- package/src/ui/image-editor/canvas.ts +82 -11
- package/src/ui/image-editor/math.ts +78 -0
- package/src/ui/shell.ts +11 -0
package/README.md
CHANGED
|
@@ -13,12 +13,53 @@ Live demos for everything: run `npm run dev:showcase` → **Develop → Componen
|
|
|
13
13
|
|
|
14
14
|
## Subpath surfaces
|
|
15
15
|
|
|
16
|
+
The `client/runtime`, `ui/shell`, `admin/registry`, and Assistant launcher
|
|
17
|
+
subpaths are stable, narrow package boundaries for first-party portal shells
|
|
18
|
+
and advanced hosts. They intentionally expose only the runtime pieces needed
|
|
19
|
+
at application startup; use the broader compatibility surfaces when those
|
|
20
|
+
additional exports are required.
|
|
21
|
+
|
|
16
22
|
| Subpath | Surface |
|
|
17
23
|
|---|---|
|
|
18
24
|
| `portal-mojo/client` | Typed django-mojo protocol layer: envelope unwrap at exactly one boundary (a failed save **rejects**), `start`/`size` paging, `'-field'` sort, Django lookups, the URL-synced `useTableParams` store (single source of truth for table state), TanStack Query hooks. Auth client: password / magic-link / passkey login, forgot/reset, cross-origin handoff (`?auth_code=` scrub-before-network), single-flight refresh + pre-request gate (synthetic-401 reject, `/api/token/refresh` recursion guard), `X-Mojo-UID` device header, `getAuthSnapshot`/`subscribeAuth` for React. Auth-challenged realtime transport: `RealtimeProvider`/`RealtimeClient`, refcounted topics, typed event projection, and a deterministic mock. Boot auth with `initAuth()`. The in-memory HTTP mock transport lives here too — it is the wire contract's executable spec and evolves in lockstep with the client (any seeded active user logs in with email + `"mojo"`). |
|
|
25
|
+
| `portal-mojo/client/runtime` | Stable, narrow client boundary for first-party application runtime code. It omits eager mock-test controls; the mock transport itself loads on the first mock request. |
|
|
19
26
|
| `portal-mojo/ui` | Mission-control UI: `ModelTable`, `FilterBar`/`FilterPills`, `SchemaForm`/`formModal`, `DetailView`, `RecordFeed`, `AttachmentQueue`/`UploadQueue`, `ImageEditor`/`imageEditorModal`, awaitable native-`<dialog>` `modal`, `toast`, `ThemeProvider`, `Guarded` (permission slot), `GroupSwitcher` (searchable tree selector), `RequiresGroup`, the sidebar engine (`registerMenus`/`setDefaultMenu` registry + `SidebarNav` — static or searchable accordion with a compact icon rail; global and group scopes are explicit), `Badge`/`MetricCard`/`Spark`, `fmt` formatter namespace. |
|
|
27
|
+
| `portal-mojo/ui/shell` | Stable, narrow boundary for application-shell primitives: theme, guards, menus/sidebar, right panel, modal/toast hosts, password helpers, and formatters. |
|
|
20
28
|
| `portal-mojo/charts` | Dependency-free SVG charts: `SeriesChart` (line/bar/area, stacked bars, legend toggle, crosshair tooltip) and `MetricsChart` (granularity/range/type control bar for `/api/metrics/fetch`). |
|
|
21
|
-
| `portal-mojo/admin` |
|
|
29
|
+
| `portal-mojo/admin/core` | Side-effect-light Admin contracts and host: `AdminSection`, the discriminated `AdminRoute` XOR, `adminSectionRoutes`, `adminSectionsMenu`, and `AdminLazyPage`. |
|
|
30
|
+
| `portal-mojo/admin/registry` | Stable, narrow built-in `ADMIN_SECTIONS` roster for first-party hosts, without broad page barrels. It installs the optional Email→DNS adapter synchronously so communications routes work before DNS navigation. |
|
|
31
|
+
| `portal-mojo/admin/assistant/launcher` | Stable, narrow shell launcher boundary without the Assistant page/data barrel. |
|
|
32
|
+
| `portal-mojo/admin/{identity,security,observability,operations,infrastructure,communications,assistant}` | Narrow domain APIs and registry contributions. Built-in routes use leaf-module `loadComponent` loaders; importing `infrastructure` synchronously installs the optional Email→DNS adapter. |
|
|
33
|
+
| `portal-mojo/admin` | Compatibility aggregate containing every historical named export and the stable `ADMIN_SECTIONS` roster. It intentionally installs the DNS adapter and Rule field registration synchronously. New code should prefer `admin/core` plus the domains it consumes. |
|
|
34
|
+
|
|
35
|
+
### Admin route contract
|
|
36
|
+
|
|
37
|
+
Routes are an explicit XOR. A host-owned page stays synchronous; a packaged
|
|
38
|
+
page supplies a separately named loader. The host never guesses from whether a
|
|
39
|
+
value is callable.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const extension: AdminSection = {
|
|
43
|
+
id: 'billing', title: 'Billing', icon: 'bi-receipt',
|
|
44
|
+
permissions: ['sys.billing'],
|
|
45
|
+
routes: [{ path: '', component: BillingHome }],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const packaged: AdminSection = {
|
|
49
|
+
id: 'audit', title: 'Audit', icon: 'bi-journal-text',
|
|
50
|
+
permissions: ['sys.view_logs'],
|
|
51
|
+
routes: [{
|
|
52
|
+
path: '',
|
|
53
|
+
loadComponent: () => import('./AuditPage').then(({ AuditPage }) => ({ default: AuditPage })),
|
|
54
|
+
}],
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Lazy loading, chunk-error recovery, and retry render inside section and route
|
|
59
|
+
permission guards, so a denied route never invokes its loader. Routes stay
|
|
60
|
+
mount-relative: pass no mount for `#/…`, or `{mount: '/system'}` for the same
|
|
61
|
+
sections under `#/system/…`. Root denial and first-visible fallback semantics
|
|
62
|
+
are identical in both forms.
|
|
22
63
|
|
|
23
64
|
## What the consuming app must provide
|
|
24
65
|
|
package/docs/admin-assistant.md
CHANGED
|
@@ -9,20 +9,22 @@ Import from `portal-mojo/admin`.
|
|
|
9
9
|
- An owned, text-only conversation uses realtime only when the shared transport
|
|
10
10
|
is authenticated and the caller locally has `sys.view_admin`, matching the
|
|
11
11
|
WebSocket handler's narrower server check. The client sends exactly
|
|
12
|
-
`{type:'assistant_message',message,conversation_id?}
|
|
12
|
+
`{type:'assistant_message',message,request_id,conversation_id?}`, with a fresh
|
|
13
|
+
canonical UUID per turn. A caller with only
|
|
13
14
|
`sys.assistant`, any send with attachments, an unavailable socket, and a
|
|
14
15
|
foreign inspect-only conversation stay on REST. Messages are never
|
|
15
16
|
auto-resubmitted across paths.
|
|
16
17
|
- `POST /api/assistant` sends `{message, conversation_id?, attachments?}`.
|
|
17
18
|
`attachments` is omitted for text-only sends or contains 1–5 unique positive
|
|
18
19
|
ids from completed authoritative queue references.
|
|
19
|
-
- Realtime
|
|
20
|
+
- Realtime accepts these Assistant event shapes only as exact direct frames;
|
|
21
|
+
topic-wrapped or broadcast Assistant-shaped traffic is ignored. It positively
|
|
22
|
+
projects only `assistant_thinking`, `assistant_text`,
|
|
20
23
|
`assistant_tool_call`, `assistant_plan`, `assistant_plan_update`,
|
|
21
|
-
`assistant_response`, and `assistant_error
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
uncorrelated server error terminates the active turn. Tool state retains
|
|
24
|
+
`assistant_response`, and `assistant_error`. Thinking, text, tool, plan,
|
|
25
|
+
update, response, and error events require the exact `request_id` sent for
|
|
26
|
+
the active turn. Non-error events also require a positive `conversation_id`;
|
|
27
|
+
an error may omit only the conversation id. Tool state retains
|
|
26
28
|
bounded name/status/count only; raw input and the terminal `tool_calls_made`
|
|
27
29
|
list never enter UI state, caches, logs, or mock observations. Plans omit
|
|
28
30
|
tool inputs. Terminal duplication is checked only with the backend's
|
|
@@ -37,6 +39,11 @@ Import from `portal-mojo/admin`.
|
|
|
37
39
|
reconnect; locally observed permission loss immediately clears this
|
|
38
40
|
consumer's transient stream/subscriptions without disabling the shared
|
|
39
41
|
provider.
|
|
42
|
+
- Direct-source binding and the echoed `request_id` jointly isolate concurrent
|
|
43
|
+
turns, including turns in the same conversation. Missing or mismatched IDs
|
|
44
|
+
are ignored; if no correlated acknowledgement arrives within ten seconds,
|
|
45
|
+
the outcome is marked unknown, the list is refreshed, and the send is never
|
|
46
|
+
retried automatically.
|
|
40
47
|
- `POST /api/assistant/context` sends only `{model, pk}`. The returned conversation id is immediately fetched from `/api/assistant/conversation/<id>?graph=detail`; the client never synthesizes or reposts context text.
|
|
41
48
|
- Conversation and Skill lists/details/deletes are imperative and component-local. They do not use `defineModel`, Query cache, `ModelTable`, a `RecordFeed` adapter, persistence, or exports.
|
|
42
49
|
- A foreign conversation visible to an administrator is inspect-only. Only `conversation.user.id === me.id` enables continuation.
|
package/docs/image-editor.md
CHANGED
|
@@ -64,6 +64,19 @@ These helpers operate on logical pixels and do not mount UI, upload Files,
|
|
|
64
64
|
persist state, or initiate a browser download. `decodeImageSource` is the one
|
|
65
65
|
exception that performs the URL fetch described above.
|
|
66
66
|
|
|
67
|
+
## Browser-work limits
|
|
68
|
+
|
|
69
|
+
The public low-level constants make the editor's availability boundary explicit:
|
|
70
|
+
`MAX_IMAGE_SOURCE_BYTES` is 25,000,000 bytes, `MAX_IMAGE_DIMENSION` is 8,192
|
|
71
|
+
pixels, `MAX_IMAGE_PIXELS` is 16,000,000 pixels per decoded/intermediate/final
|
|
72
|
+
surface, and `MAX_IMAGE_EDITOR_OPERATIONS` is 20. Every operation output is
|
|
73
|
+
checked before raster allocation. Initial transforms and filters must remain
|
|
74
|
+
inside the same ranges as the controls (scale 0.1–5; brightness, contrast, and
|
|
75
|
+
saturation 0–200; hue 0–360; blur 0–10; grayscale and sepia 0–100). URL bodies
|
|
76
|
+
are streamed to the byte ceiling, and source replacement or unmount aborts the
|
|
77
|
+
fetch and `<img>` fallback where the browser permits; `createImageBitmap` itself
|
|
78
|
+
is not abortable, so a stale result is closed immediately on resolution.
|
|
79
|
+
|
|
67
80
|
## Composition and history
|
|
68
81
|
|
|
69
82
|
Operations are immutable and chronological. Transform creates a logical
|
package/docs/menus-and-access.md
CHANGED
|
@@ -58,6 +58,20 @@ Admin section bundles contribute through `adminSectionsMenu`. Use
|
|
|
58
58
|
experience. Routes remain mount-relative; multiple embedded root sections
|
|
59
59
|
share one `/system` landing instead of emitting duplicate index routes.
|
|
60
60
|
|
|
61
|
+
Import the route/menu machinery from `portal-mojo/admin/core`, then import only
|
|
62
|
+
the registry domains the shell installs. The stable domains are `identity`,
|
|
63
|
+
`security`, `observability`, `operations`, `infrastructure`, `communications`,
|
|
64
|
+
and `assistant`. `portal-mojo/admin` remains the compatibility aggregate and
|
|
65
|
+
retains every historical export, but deliberately performs the legacy DNS and
|
|
66
|
+
Rule-field setup at import time.
|
|
67
|
+
|
|
68
|
+
Built-in routes use `loadComponent`; external sections may use `component`.
|
|
69
|
+
These properties are mutually exclusive. Loading and retryable error states
|
|
70
|
+
live inside both permission guards, so access denial performs no page import.
|
|
71
|
+
The infrastructure entry installs the optional managed-DNS adapter
|
|
72
|
+
synchronously, which makes Email-first navigation safe even before any DNS page
|
|
73
|
+
chunk has loaded. `admin/core` performs neither DNS nor field registration.
|
|
74
|
+
|
|
61
75
|
## Permissions (A2) — client gates, server authority
|
|
62
76
|
|
|
63
77
|
`Me.permissions` is a dict; truthy `true` OR `1` grants. Semantics
|
package/docs/realtime.md
CHANGED
|
@@ -28,8 +28,10 @@ disable this shared provider.
|
|
|
28
28
|
separately and is never explicitly subscribed by the client. That automatic
|
|
29
29
|
topic is a delivery scope, not a presence feed.
|
|
30
30
|
- Framework messages, one wrapped `{type:'message',data,topic?,timestamp?}`
|
|
31
|
-
layer, and direct application events share one dispatch boundary.
|
|
32
|
-
|
|
31
|
+
layer, and direct application events share one dispatch boundary. Every
|
|
32
|
+
projected event carries `source: 'wrapped' | 'direct'` so consumers can keep
|
|
33
|
+
capability-bearing protocols on their exact wire source. Malformed frames
|
|
34
|
+
drop. No generic Query invalidation is performed.
|
|
33
35
|
|
|
34
36
|
The transport does not invent application events. Current django-mojo has no
|
|
35
37
|
canonical record-created/updated/deleted stream and no canonical presence
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-mojo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "React portal toolkit for django-mojo — typed client + params store, mission-control UI, dependency-free SVG charts, admin section bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -26,9 +26,21 @@
|
|
|
26
26
|
},
|
|
27
27
|
"exports": {
|
|
28
28
|
"./client": "./src/client/index.ts",
|
|
29
|
+
"./client/runtime": "./src/client/runtime.ts",
|
|
29
30
|
"./ui": "./src/ui/index.ts",
|
|
31
|
+
"./ui/shell": "./src/ui/shell.ts",
|
|
30
32
|
"./charts": "./src/charts/index.ts",
|
|
31
|
-
"./admin": "./src/admin/index.ts"
|
|
33
|
+
"./admin": "./src/admin/index.ts",
|
|
34
|
+
"./admin/core": "./src/admin/core/index.ts",
|
|
35
|
+
"./admin/registry": "./src/admin/registry.ts",
|
|
36
|
+
"./admin/identity": "./src/admin/public/identity.ts",
|
|
37
|
+
"./admin/security": "./src/admin/public/security.ts",
|
|
38
|
+
"./admin/observability": "./src/admin/public/observability.ts",
|
|
39
|
+
"./admin/operations": "./src/admin/public/operations.ts",
|
|
40
|
+
"./admin/infrastructure": "./src/admin/public/infrastructure.ts",
|
|
41
|
+
"./admin/communications": "./src/admin/public/communications.ts",
|
|
42
|
+
"./admin/assistant": "./src/admin/public/assistant.ts",
|
|
43
|
+
"./admin/assistant/launcher": "./src/admin/public/assistant-launcher.ts"
|
|
32
44
|
},
|
|
33
45
|
"scripts": {
|
|
34
46
|
"typecheck": "tsc --noEmit"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, useState, type MouseEvent } from 'react';
|
|
2
|
+
import { useCan } from '../../client/runtime';
|
|
3
|
+
import { useRightPanel } from '../../ui/RightPanel';
|
|
4
|
+
import { ASSISTANT_PERMISSIONS } from './permissions';
|
|
5
|
+
|
|
6
|
+
export function AssistantLauncher() {
|
|
7
|
+
const { can } = useCan(ASSISTANT_PERMISSIONS);
|
|
8
|
+
const panel = useRightPanel();
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
useEffect(() => { if (!can && panel.descriptor?.key.startsWith('assistant:')) panel.close(); }, [can, panel]);
|
|
12
|
+
if (!can) return null;
|
|
13
|
+
const open = async (event: MouseEvent<HTMLButtonElement>) => {
|
|
14
|
+
const launcher = event.currentTarget;
|
|
15
|
+
setLoading(true); setError('');
|
|
16
|
+
try {
|
|
17
|
+
const { AssistantPanel } = await import('./AssistantPanel');
|
|
18
|
+
panel.open({ key: 'assistant:new', title: 'Assistant', render: (context) => <AssistantPanel context={context} /> }, launcher);
|
|
19
|
+
} catch { setError('Assistant could not load. Click to retry.'); }
|
|
20
|
+
finally { setLoading(false); }
|
|
21
|
+
};
|
|
22
|
+
return <button type="button" className="btn-icon" title={error || 'Open Assistant'} aria-label={error || 'Open Assistant'} aria-pressed={panel.descriptor?.key === 'assistant:new'} disabled={loading} onClick={(event) => void open(event)}><i className="bi bi-stars" aria-hidden="true" /></button>;
|
|
23
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useCan, useMe, useRealtime, useRealtimeStatus } from '../../client';
|
|
2
|
+
import { useCan, useMe, useRealtime, useRealtimeStatus } from '../../client/runtime';
|
|
3
3
|
import type { RightPanelRenderContext } from '../../ui';
|
|
4
4
|
import { AssistantFeed } from './AssistantFeed';
|
|
5
5
|
import { sendAssistantMessage } from './api';
|
|
6
6
|
import type { AssistantConversation, AssistantMessage } from './types';
|
|
7
7
|
import type { FileReference } from '../../client/record-feed';
|
|
8
8
|
import { AssistantOutcomeUnknownError, chooseAssistantTransport, startAssistantRealtimeTurn, type AssistantRealtimeProgress, type AssistantRealtimeTurn } from './streaming';
|
|
9
|
+
import { ASSISTANT_PERMISSIONS } from './permissions';
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
+
export { ASSISTANT_PERMISSIONS } from './permissions';
|
|
11
12
|
|
|
12
13
|
export function AssistantPanel({ context, initialConversation = null }: { context: RightPanelRenderContext; initialConversation?: AssistantConversation | null }) {
|
|
13
14
|
const { can } = useCan(ASSISTANT_PERMISSIONS);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mojoCall, mojoDelete, mojoList, type Params } from '../../client';
|
|
1
|
+
import { mojoCall, mojoDelete, mojoList, type Params } from '../../client/runtime';
|
|
2
2
|
import { projectConversation, projectConversationSummary, projectReply, projectSkill } from './data';
|
|
3
3
|
import type { AssistantConversation, AssistantConversationSummary, AssistantReply, AssistantSkill } from './types';
|
|
4
4
|
import type { FileReference } from '../../client/record-feed';
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { AdminSection } from '../index';
|
|
2
|
-
import { ASSISTANT_PERMISSIONS } from './AssistantPanel';
|
|
3
|
-
import { ConversationsPage, MemoriesPage, SkillsPage } from './pages';
|
|
4
1
|
|
|
5
2
|
export * from './types';
|
|
6
3
|
export * from './data';
|
|
@@ -11,11 +8,4 @@ export * from './launchers';
|
|
|
11
8
|
export * from './pages';
|
|
12
9
|
export * from './streaming';
|
|
13
10
|
|
|
14
|
-
export
|
|
15
|
-
id: 'assistant', basePath: 'assistant', title: 'Assistant', icon: 'bi-stars', navigationGroup: 'assistant', permissions: ASSISTANT_PERMISSIONS,
|
|
16
|
-
routes: [
|
|
17
|
-
{ path: 'conversations', label: 'Conversations', component: ConversationsPage, permissions: ASSISTANT_PERMISSIONS },
|
|
18
|
-
{ path: 'skills', label: 'Skills', component: SkillsPage, permissions: ASSISTANT_PERMISSIONS },
|
|
19
|
-
{ path: 'memories', label: 'Memory', component: MemoriesPage, permissions: ['sys.assistant'] },
|
|
20
|
-
],
|
|
21
|
-
};
|
|
11
|
+
export { ASSISTANT_ADMIN_SECTION } from '../domains/assistant';
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type MouseEvent } from 'react';
|
|
2
|
-
import { useCan } from '../../client';
|
|
3
|
-
import { useRightPanel } from '../../ui';
|
|
2
|
+
import { useCan } from '../../client/runtime';
|
|
3
|
+
import { useRightPanel } from '../../ui/RightPanel';
|
|
4
4
|
import { SECURITY_VIEW_PERMS } from '../security-permissions';
|
|
5
|
-
import {
|
|
5
|
+
import { ASSISTANT_PERMISSIONS } from './permissions';
|
|
6
6
|
import { createAssistantContext } from './api';
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
const { can } = useCan(ASSISTANT_PERMISSIONS);
|
|
10
|
-
const panel = useRightPanel();
|
|
11
|
-
useEffect(() => { if (!can && panel.descriptor?.key.startsWith('assistant:')) panel.close(); }, [can, panel]);
|
|
12
|
-
if (!can) return null;
|
|
13
|
-
return <button type="button" className="btn-icon" title="Open Assistant" aria-label="Open Assistant" aria-pressed={panel.descriptor?.key === 'assistant:new'} onClick={(event) => panel.open({ key: 'assistant:new', title: 'Assistant', render: (context) => <AssistantPanel context={context} /> }, event.currentTarget)}><i className="bi bi-stars" aria-hidden="true" /></button>;
|
|
14
|
-
}
|
|
8
|
+
export { AssistantLauncher } from './AssistantLauncher';
|
|
15
9
|
|
|
16
10
|
export function AssistantContextLauncher({ model, pk, label = 'Ask Assistant' }: { model: 'incident.Incident' | 'incident.Ticket'; pk: number; label?: string }) {
|
|
17
11
|
const assistant = useCan(ASSISTANT_PERMISSIONS);
|
|
@@ -25,7 +19,7 @@ export function AssistantContextLauncher({ model, pk, label = 'Ask Assistant' }:
|
|
|
25
19
|
if (!assistant.can || !security.can) return null;
|
|
26
20
|
const launch = async (event: MouseEvent<HTMLButtonElement>) => {
|
|
27
21
|
const launcher = event.currentTarget; const run = ++generation.current; setLoading(true); setError('');
|
|
28
|
-
try { const conversation = await createAssistantContext(model, pk); if (generation.current !== run) return; panel.open({ key: `assistant:context:${model}:${pk}:${conversation.id}`, title: `Assistant · ${model === 'incident.Incident' ? 'Incident' : 'Ticket'} #${pk}`, render: (context) => <AssistantPanel context={context} initialConversation={conversation} /> }, launcher); }
|
|
22
|
+
try { const [conversation, { AssistantPanel }] = await Promise.all([createAssistantContext(model, pk), import('./AssistantPanel')]); if (generation.current !== run) return; panel.open({ key: `assistant:context:${model}:${pk}:${conversation.id}`, title: `Assistant · ${model === 'incident.Incident' ? 'Incident' : 'Ticket'} #${pk}`, render: (context) => <AssistantPanel context={context} initialConversation={conversation} /> }, launcher); }
|
|
29
23
|
catch (cause) { if (generation.current === run) setError(cause instanceof Error ? cause.message : 'Could not create context'); }
|
|
30
24
|
finally { if (generation.current === run) setLoading(false); }
|
|
31
25
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { mojoList, useCan, useMe, useRealtime, useRealtimeStatus } from '../../client';
|
|
2
|
+
import { mojoList, useCan, useMe, useRealtime, useRealtimeStatus } from '../../client/runtime';
|
|
3
3
|
import { Badge, fmt, modal, toast } from '../../ui';
|
|
4
4
|
import { AssistantFeed } from './AssistantFeed';
|
|
5
5
|
import { deleteAssistantConversation, deleteAssistantMemory, deleteAssistantSkill, getAssistantConversation, getAssistantMemory, getAssistantSkill, listAssistantConversations, listAssistantSkills, saveAssistantMemory, sendAssistantMessage } from './api';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ASSISTANT_PERMISSIONS = ['sys.view_admin', 'sys.assistant'];
|
|
@@ -9,18 +9,21 @@ const MAX_TEXT = 20_000;
|
|
|
9
9
|
const MAX_TOOL_NAME = 160;
|
|
10
10
|
const MAX_TOOLS = 32;
|
|
11
11
|
const MAX_PLAN_STEPS = 20;
|
|
12
|
+
const ASSISTANT_ACK_TIMEOUT_MS = 10_000;
|
|
13
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12
14
|
const object = (value: unknown): Record<string, unknown> | null => value != null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
13
15
|
const text = (value: unknown, cap: number): string | null => typeof value === 'string' && value.length > 0 ? value.slice(0, cap) : null;
|
|
14
16
|
const conversationId = (value: unknown): number | null => typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
17
|
+
const requestId = (value: unknown): string | null => typeof value === 'string' && UUID_PATTERN.test(value) ? value : null;
|
|
15
18
|
|
|
16
|
-
export interface AssistantThinkingEvent { type: 'thinking'; conversationId: number }
|
|
17
|
-
export interface AssistantTextEvent { type: 'text'; conversationId: number; text: string; blocks: AssistantBlock[] }
|
|
18
|
-
export interface AssistantToolEvent { type: 'tool'; conversationId: number; name: string; status: 'running'; count: number }
|
|
19
|
+
export interface AssistantThinkingEvent { type: 'thinking'; conversationId: number; requestId: string }
|
|
20
|
+
export interface AssistantTextEvent { type: 'text'; conversationId: number; requestId: string; text: string; blocks: AssistantBlock[] }
|
|
21
|
+
export interface AssistantToolEvent { type: 'tool'; conversationId: number; requestId: string; name: string; status: 'running'; count: number }
|
|
19
22
|
export interface AssistantPlanStep { id: number; description: string; status: 'pending' | 'in_progress' | 'done' | 'skipped'; summary?: string }
|
|
20
|
-
export interface AssistantPlanEvent { type: 'plan'; conversationId: number; planId: string; title: string; steps: AssistantPlanStep[] }
|
|
21
|
-
export interface AssistantPlanUpdateEvent { type: 'plan_update'; conversationId: number; planId: string; stepId: number; status: AssistantPlanStep['status']; summary?: string }
|
|
22
|
-
export interface AssistantResponseEvent { type: 'response'; conversationId: number; messageId: number | string | null; created: number | string; response: string; blocks: AssistantBlock[]; toolCount: number; durationMs: number }
|
|
23
|
-
export interface AssistantErrorEvent { type: 'error'; conversationId: number | null; error: string }
|
|
23
|
+
export interface AssistantPlanEvent { type: 'plan'; conversationId: number; requestId: string; planId: string; title: string; steps: AssistantPlanStep[] }
|
|
24
|
+
export interface AssistantPlanUpdateEvent { type: 'plan_update'; conversationId: number; requestId: string; planId: string; stepId: number; status: AssistantPlanStep['status']; summary?: string }
|
|
25
|
+
export interface AssistantResponseEvent { type: 'response'; conversationId: number; requestId: string; messageId: number | string | null; created: number | string; response: string; blocks: AssistantBlock[]; toolCount: number; durationMs: number }
|
|
26
|
+
export interface AssistantErrorEvent { type: 'error'; conversationId: number | null; requestId: string; error: string }
|
|
24
27
|
export type AssistantProjectedEvent = AssistantThinkingEvent | AssistantTextEvent | AssistantToolEvent | AssistantPlanEvent | AssistantPlanUpdateEvent | AssistantResponseEvent | AssistantErrorEvent;
|
|
25
28
|
|
|
26
29
|
function exact(value: unknown, expectedType: string): Record<string, unknown> | null {
|
|
@@ -29,18 +32,20 @@ function exact(value: unknown, expectedType: string): Record<string, unknown> |
|
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export function projectAssistantThinking(value: unknown): AssistantThinkingEvent | null {
|
|
32
|
-
const raw = exact(value, 'assistant_thinking'); const id = raw && conversationId(raw.conversation_id);
|
|
33
|
-
return id == null ? null : { type: 'thinking', conversationId: id };
|
|
35
|
+
const raw = exact(value, 'assistant_thinking'); const id = raw && conversationId(raw.conversation_id); const turn = raw && requestId(raw.request_id);
|
|
36
|
+
return id == null || turn == null ? null : { type: 'thinking', conversationId: id, requestId: turn };
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export function projectAssistantText(value: unknown): AssistantTextEvent | null {
|
|
37
|
-
const raw = exact(value, 'assistant_text');
|
|
38
|
-
|
|
40
|
+
const raw = exact(value, 'assistant_text');
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
const id = conversationId(raw.conversation_id); const turn = requestId(raw.request_id); const content = text(raw.text, MAX_TEXT);
|
|
43
|
+
return id == null || turn == null || content == null ? null : { type: 'text', conversationId: id, requestId: turn, text: content, blocks: projectBlocks(raw.blocks) };
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
export function projectAssistantToolCall(value: unknown): AssistantToolEvent | null {
|
|
42
|
-
const raw = exact(value, 'assistant_tool_call'); const id = raw && conversationId(raw.conversation_id); const name = raw && text(raw.tool, MAX_TOOL_NAME);
|
|
43
|
-
return id == null || name == null ? null : { type: 'tool', conversationId: id, name, status: 'running', count: 1 };
|
|
47
|
+
const raw = exact(value, 'assistant_tool_call'); const id = raw && conversationId(raw.conversation_id); const turn = raw && requestId(raw.request_id); const name = raw && text(raw.tool, MAX_TOOL_NAME);
|
|
48
|
+
return id == null || turn == null || name == null ? null : { type: 'tool', conversationId: id, requestId: turn, name, status: 'running', count: 1 };
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
const PLAN_STATUSES = new Set<AssistantPlanStep['status']>(['pending', 'in_progress', 'done', 'skipped']);
|
|
@@ -49,38 +54,40 @@ function planStatus(value: unknown): AssistantPlanStep['status'] | null {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export function projectAssistantPlan(value: unknown): AssistantPlanEvent | null {
|
|
52
|
-
const raw = exact(value, 'assistant_plan'); const id = raw && conversationId(raw.conversation_id); const plan = raw && object(raw.plan);
|
|
57
|
+
const raw = exact(value, 'assistant_plan'); const id = raw && conversationId(raw.conversation_id); const turn = raw && requestId(raw.request_id); const plan = raw && object(raw.plan);
|
|
53
58
|
const planId = plan && text(plan.plan_id, 80); const title = plan && text(plan.title, 200);
|
|
54
|
-
if (id == null || !plan || !planId || !title || !Array.isArray(plan.steps) || plan.steps.length < 1 || plan.steps.length > MAX_PLAN_STEPS) return null;
|
|
59
|
+
if (id == null || turn == null || !plan || !planId || !title || !Array.isArray(plan.steps) || plan.steps.length < 1 || plan.steps.length > MAX_PLAN_STEPS) return null;
|
|
55
60
|
const steps = plan.steps.map(object).map((step) => {
|
|
56
61
|
const stepId = step && conversationId(step.id); const description = step && text(step.description, 500); const status = step && planStatus(step.status);
|
|
57
62
|
if (stepId == null || !description || !status) return null;
|
|
58
63
|
const summary = text(step.summary, 500);
|
|
59
64
|
return { id: stepId, description, status, ...(summary ? { summary } : {}) };
|
|
60
65
|
});
|
|
61
|
-
return steps.every(Boolean) ? { type: 'plan', conversationId: id, planId, title, steps: steps as AssistantPlanStep[] } : null;
|
|
66
|
+
return steps.every(Boolean) ? { type: 'plan', conversationId: id, requestId: turn, planId, title, steps: steps as AssistantPlanStep[] } : null;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export function projectAssistantPlanUpdate(value: unknown): AssistantPlanUpdateEvent | null {
|
|
65
|
-
const raw = exact(value, 'assistant_plan_update'); const id = raw && conversationId(raw.conversation_id); const planId = raw && text(raw.plan_id, 80);
|
|
70
|
+
const raw = exact(value, 'assistant_plan_update'); const id = raw && conversationId(raw.conversation_id); const turn = raw && requestId(raw.request_id); const planId = raw && text(raw.plan_id, 80);
|
|
66
71
|
const stepId = raw && conversationId(raw.step_id); const status = raw && planStatus(raw.status); const summary = raw && text(raw.summary, 500);
|
|
67
|
-
return id == null || !planId || stepId == null || !status ? null : { type: 'plan_update', conversationId: id, planId, stepId, status, ...(summary ? { summary } : {}) };
|
|
72
|
+
return id == null || turn == null || !planId || stepId == null || !status ? null : { type: 'plan_update', conversationId: id, requestId: turn, planId, stepId, status, ...(summary ? { summary } : {}) };
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
export function projectAssistantResponse(value: unknown): AssistantResponseEvent | null {
|
|
71
|
-
const raw = exact(value, 'assistant_response');
|
|
72
|
-
if (
|
|
76
|
+
const raw = exact(value, 'assistant_response');
|
|
77
|
+
if (!raw) return null;
|
|
78
|
+
const id = conversationId(raw.conversation_id); const turn = requestId(raw.request_id); const response = typeof raw.response === 'string' ? raw.response.slice(0, MAX_TEXT) : null;
|
|
79
|
+
if (id == null || turn == null || response == null) return null;
|
|
73
80
|
const messageId = typeof raw.message_id === 'number' && Number.isSafeInteger(raw.message_id) && raw.message_id > 0
|
|
74
81
|
? raw.message_id : typeof raw.message_id === 'string' && raw.message_id.length > 0 && raw.message_id.length <= 160 ? raw.message_id : null;
|
|
75
82
|
const created = typeof raw.created === 'number' || typeof raw.created === 'string' ? raw.created : 0;
|
|
76
83
|
const toolCount = Array.isArray(raw.tool_calls_made) ? Math.min(raw.tool_calls_made.length, MAX_TOOLS) : typeof raw.tool_calls_made === 'number' ? Math.min(Math.max(0, raw.tool_calls_made), MAX_TOOLS) : 0;
|
|
77
84
|
const durationMs = typeof raw.duration_ms === 'number' && Number.isFinite(raw.duration_ms) ? Math.max(0, raw.duration_ms) : 0;
|
|
78
|
-
return { type: 'response', conversationId: id, messageId, created, response, blocks: projectBlocks(raw.blocks), toolCount, durationMs };
|
|
85
|
+
return { type: 'response', conversationId: id, requestId: turn, messageId, created, response, blocks: projectBlocks(raw.blocks), toolCount, durationMs };
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export function projectAssistantError(value: unknown): AssistantErrorEvent | null {
|
|
82
|
-
const raw = exact(value, 'assistant_error'); const error = raw && text(raw.error, 1_000);
|
|
83
|
-
return !raw || !error ? null : { type: 'error', conversationId: conversationId(raw.conversation_id), error };
|
|
89
|
+
const raw = exact(value, 'assistant_error'); const turn = raw && requestId(raw.request_id); const error = raw && text(raw.error, 1_000);
|
|
90
|
+
return !raw || turn == null || !error ? null : { type: 'error', conversationId: conversationId(raw.conversation_id), requestId: turn, error };
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
export const ASSISTANT_REALTIME_EVENTS = {
|
|
@@ -100,8 +107,8 @@ export function chooseAssistantTransport(input: { owner: boolean; textOnly: bool
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
export class AssistantOutcomeUnknownError extends Error {
|
|
103
|
-
constructor() {
|
|
104
|
-
super(
|
|
110
|
+
constructor(message = 'Outcome unknown: the connection ended before the server assigned a conversation. The conversation list was refreshed; the message was not resent.') {
|
|
111
|
+
super(message);
|
|
105
112
|
this.name = 'AssistantOutcomeUnknownError';
|
|
106
113
|
}
|
|
107
114
|
}
|
|
@@ -133,12 +140,15 @@ export function startAssistantRealtimeTurn(
|
|
|
133
140
|
seenMessageIds: Set<number | string>;
|
|
134
141
|
reconcile?: (conversationId: number) => Promise<AssistantConversation>;
|
|
135
142
|
refreshList?: () => Promise<unknown>;
|
|
143
|
+
ackTimeoutMs?: number;
|
|
136
144
|
},
|
|
137
145
|
callbacks: AssistantRealtimeTurnCallbacks,
|
|
138
146
|
): AssistantRealtimeTurn {
|
|
147
|
+
const turnRequestId = crypto.randomUUID();
|
|
139
148
|
let expected = input.conversationId ?? null;
|
|
140
149
|
let active = true;
|
|
141
150
|
let disconnected = false;
|
|
151
|
+
let acknowledged = false;
|
|
142
152
|
let interimSequence = 0;
|
|
143
153
|
const tools = new Map<string, { name: string; status: 'running'; count: number }>();
|
|
144
154
|
let plan: AssistantPlanEvent | null = null;
|
|
@@ -153,39 +163,48 @@ export function startAssistantRealtimeTurn(
|
|
|
153
163
|
cleanups.splice(0).forEach((cleanup) => cleanup());
|
|
154
164
|
if (error) rejectPromise(error); else resolvePromise();
|
|
155
165
|
};
|
|
156
|
-
const
|
|
166
|
+
const acknowledge = () => {
|
|
167
|
+
if (!acknowledged) acknowledged = true;
|
|
168
|
+
};
|
|
169
|
+
const correlated = (id: number | null, eventRequestId: string): id is number => eventRequestId === turnRequestId && id != null && (expected == null || expected === id);
|
|
157
170
|
const adopt = (id: number) => {
|
|
158
171
|
if (expected == null) { expected = id; callbacks.onConversation(id); }
|
|
159
172
|
};
|
|
160
173
|
const progress = () => callbacks.onProgress({ tools: [...tools.values()].slice(0, MAX_TOOLS), plan });
|
|
161
174
|
|
|
162
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.thinking, ({ data }) => {
|
|
163
|
-
if (!active || !correlated(data.conversationId)) return;
|
|
175
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.thinking, ({ data, source }) => {
|
|
176
|
+
if (source !== 'direct' || !active || !correlated(data.conversationId, data.requestId)) return;
|
|
177
|
+
acknowledge();
|
|
164
178
|
adopt(data.conversationId);
|
|
165
179
|
}));
|
|
166
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.text, ({ data }) => {
|
|
167
|
-
if (!active || !correlated(data.conversationId)) return;
|
|
180
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.text, ({ data, source }) => {
|
|
181
|
+
if (source !== 'direct' || !active || !correlated(data.conversationId, data.requestId)) return;
|
|
182
|
+
acknowledge();
|
|
168
183
|
adopt(data.conversationId);
|
|
169
184
|
callbacks.onText({ id: `stream-${data.conversationId}-${++interimSequence}`, role: 'assistant', content: data.text, created: Math.floor(Date.now() / 1000), blocks: data.blocks });
|
|
170
185
|
}));
|
|
171
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.tool, ({ data }) => {
|
|
172
|
-
if (!active || !correlated(data.conversationId)) return;
|
|
186
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.tool, ({ data, source }) => {
|
|
187
|
+
if (source !== 'direct' || !active || !correlated(data.conversationId, data.requestId)) return;
|
|
188
|
+
acknowledge();
|
|
173
189
|
adopt(data.conversationId);
|
|
174
190
|
const current = tools.get(data.name);
|
|
175
191
|
tools.set(data.name, { name: data.name, status: 'running', count: Math.min(MAX_TOOLS, (current?.count ?? 0) + 1) });
|
|
176
192
|
progress();
|
|
177
193
|
}));
|
|
178
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.plan, ({ data }) => {
|
|
179
|
-
if (!active || !correlated(data.conversationId)) return;
|
|
194
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.plan, ({ data, source }) => {
|
|
195
|
+
if (source !== 'direct' || !active || !correlated(data.conversationId, data.requestId)) return;
|
|
196
|
+
acknowledge();
|
|
180
197
|
adopt(data.conversationId); plan = data; progress();
|
|
181
198
|
}));
|
|
182
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.planUpdate, ({ data }) => {
|
|
183
|
-
if (!active || !correlated(data.conversationId) || !plan || plan.planId !== data.planId) return;
|
|
199
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.planUpdate, ({ data, source }) => {
|
|
200
|
+
if (source !== 'direct' || !active || !correlated(data.conversationId, data.requestId) || !plan || plan.planId !== data.planId) return;
|
|
201
|
+
acknowledge();
|
|
184
202
|
plan = { ...plan, steps: plan.steps.map((step) => step.id === data.stepId ? { ...step, status: data.status, ...(data.summary ? { summary: data.summary } : {}) } : step) };
|
|
185
203
|
progress();
|
|
186
204
|
}));
|
|
187
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.response, ({ data }) => {
|
|
188
|
-
if (!active || !correlated(data.conversationId)) return;
|
|
205
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.response, ({ data, source }) => {
|
|
206
|
+
if (source !== 'direct' || !active || !correlated(data.conversationId, data.requestId)) return;
|
|
207
|
+
acknowledge();
|
|
189
208
|
adopt(data.conversationId);
|
|
190
209
|
if (data.messageId == null) {
|
|
191
210
|
void (input.reconcile ?? getAssistantConversation)(data.conversationId).then((conversation) => {
|
|
@@ -202,8 +221,9 @@ export function startAssistantRealtimeTurn(
|
|
|
202
221
|
}
|
|
203
222
|
finish();
|
|
204
223
|
}));
|
|
205
|
-
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.error, ({ data }) => {
|
|
206
|
-
if (!active || (data.conversationId != null && !correlated(data.conversationId))) return;
|
|
224
|
+
cleanups.push(client.on(ASSISTANT_REALTIME_EVENTS.error, ({ data, source }) => {
|
|
225
|
+
if (source !== 'direct' || !active || data.requestId !== turnRequestId || (data.conversationId != null && !correlated(data.conversationId, data.requestId))) return;
|
|
226
|
+
acknowledge();
|
|
207
227
|
if (data.conversationId != null) adopt(data.conversationId);
|
|
208
228
|
finish(new Error(data.error));
|
|
209
229
|
}));
|
|
@@ -229,8 +249,16 @@ export function startAssistantRealtimeTurn(
|
|
|
229
249
|
}
|
|
230
250
|
}));
|
|
231
251
|
|
|
252
|
+
const ackTimer = window.setTimeout(() => {
|
|
253
|
+
if (!active || acknowledged) return;
|
|
254
|
+
callbacks.onUnknown();
|
|
255
|
+
void (input.refreshList ?? listAssistantConversations)().catch(() => undefined);
|
|
256
|
+
finish(new AssistantOutcomeUnknownError('Outcome unknown: the server did not return a correlated Assistant acknowledgement. The conversation list was refreshed; the message was not resent.'));
|
|
257
|
+
}, input.ackTimeoutMs ?? ASSISTANT_ACK_TIMEOUT_MS);
|
|
258
|
+
cleanups.push(() => window.clearTimeout(ackTimer));
|
|
259
|
+
|
|
232
260
|
try {
|
|
233
|
-
client.send({ type: 'assistant_message', message: input.message, ...(input.conversationId != null ? { conversation_id: input.conversationId } : {}) });
|
|
261
|
+
client.send({ type: 'assistant_message', message: input.message, request_id: turnRequestId, ...(input.conversationId != null ? { conversation_id: input.conversationId } : {}) });
|
|
234
262
|
} catch (cause) {
|
|
235
263
|
finish(cause instanceof Error ? cause : new Error('Realtime Assistant send failed'));
|
|
236
264
|
}
|
|
@@ -1,34 +1,7 @@
|
|
|
1
|
-
import type { AdminSection } from '../index';
|
|
2
|
-
import { BOUNCER_VIEW_PERMS } from './models';
|
|
3
|
-
import { BouncerSignalsPage } from './signals';
|
|
4
|
-
import { BouncerDevicesPage } from './devices';
|
|
5
|
-
import { BotSignaturesPage } from './signatures';
|
|
6
1
|
|
|
7
2
|
export * from './models';
|
|
8
3
|
export * from './signals';
|
|
9
4
|
export * from './devices';
|
|
10
5
|
export * from './signatures';
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
export const BOUNCER_ADMIN_SECTION: AdminSection = {
|
|
14
|
-
id: 'bouncer',
|
|
15
|
-
basePath: 'security/bouncer',
|
|
16
|
-
title: 'Bouncer',
|
|
17
|
-
icon: 'bi-shield-check',
|
|
18
|
-
navigationGroup: 'security',
|
|
19
|
-
permissions: BOUNCER_VIEW_PERMS,
|
|
20
|
-
routes: [
|
|
21
|
-
{
|
|
22
|
-
path: 'signals', label: 'Signal Decisions',
|
|
23
|
-
component: BouncerSignalsPage, permissions: BOUNCER_VIEW_PERMS,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
path: 'devices', label: 'Device Reputation',
|
|
27
|
-
component: BouncerDevicesPage, permissions: BOUNCER_VIEW_PERMS,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
path: 'signatures', label: 'Bot Signatures',
|
|
31
|
-
component: BotSignaturesPage, permissions: BOUNCER_VIEW_PERMS,
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
};
|
|
7
|
+
export { BOUNCER_ADMIN_SECTION } from '../domains/security';
|
|
@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
2
2
|
import {
|
|
3
3
|
defineModel, mojoCall,
|
|
4
4
|
type Params,
|
|
5
|
-
} from '../../client';
|
|
5
|
+
} from '../../client/runtime';
|
|
6
6
|
import { SECURITY_VIEW_PERMS } from '../security-permissions';
|
|
7
7
|
import { sanitizeIncidentRow } from '../incidents/sanitize';
|
|
8
8
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
fmt, modal, toast,
|
|
5
5
|
type BatchAction, type Column, type Field, type FilterDef, type FormData,
|
|
6
6
|
} from '../../ui';
|
|
7
|
-
import { useCan } from '../../client';
|
|
7
|
+
import { useCan } from '../../client/runtime';
|
|
8
8
|
import {
|
|
9
9
|
BOUNCER_MANAGE_PERMS, BOUNCER_SIGNATURE_TYPES, BotSignatureModel,
|
|
10
10
|
type BotSignatureRow, type BotSignatureType,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { MetricsChart } from '../../charts';
|
|
3
|
-
import { useAuthSnapshot } from '../../client';
|
|
3
|
+
import { useAuthSnapshot } from '../../client/runtime';
|
|
4
4
|
import { fmt } from '../../ui';
|
|
5
5
|
import {
|
|
6
6
|
CLOUDWATCH_GRANULARITIES, loadCloudWatchSeries,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
import { mojoCall, useAuthSnapshot, type MetricsResponse, type Params } from '../../client';
|
|
2
|
+
import { mojoCall, useAuthSnapshot, type MetricsResponse, type Params } from '../../client/runtime';
|
|
3
3
|
import type { MetricsFetchParams } from '../../charts';
|
|
4
4
|
|
|
5
5
|
export const CLOUDWATCH_PERMISSIONS = ['sys.manage_aws'];
|