@anchrd/intel-ui 0.8.7 → 0.9.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 (44) hide show
  1. package/README.md +2 -2
  2. package/package.json +5 -2
  3. package/src/agent/agent-avatar/agent-avatar.tsx +34 -0
  4. package/src/agent/agent-calendar/agent-calendar.tsx +79 -0
  5. package/src/agent/agent-chat/agent-chat.tsx +116 -0
  6. package/src/agent/agent-cron/agent-cron.ts +132 -0
  7. package/src/agent/agent-definition/agent-definition.ts +64 -0
  8. package/src/agent/agent-entry-title/agent-entry-title.ts +29 -0
  9. package/src/agent/agent-log/agent-log.tsx +159 -0
  10. package/src/agent/agent-models/agent-models.ts +63 -0
  11. package/src/agent/agent-profile/agent-profile.tsx +512 -0
  12. package/src/agent/agent-state/agent-state.ts +48 -0
  13. package/src/agent/agent.tsx +361 -0
  14. package/src/app/app-sidebar/app-sidebar.tsx +2 -2
  15. package/src/app/app-tree/app-tree.tsx +151 -23
  16. package/src/app/app.tsx +2 -2
  17. package/src/app/header-search/header-search.tsx +16 -16
  18. package/src/app/tree-move/tree-move.tsx +10 -10
  19. package/src/app/user-footer/user-footer.tsx +7 -1
  20. package/src/archive/archive.tsx +154 -0
  21. package/src/components/ui/avatar.tsx +39 -0
  22. package/src/components/ui/select.tsx +163 -0
  23. package/src/components/ui/tabs.tsx +52 -0
  24. package/src/data/agent-runtime/agent-runtime.ts +93 -0
  25. package/src/data/intel-data-provider/intel-data-provider.ts +206 -120
  26. package/src/data/intel-data-provider/intel-data-provider.types.ts +112 -54
  27. package/src/entry-picker/entry-picker.tsx +53 -17
  28. package/src/flow-runs/flow-runs.tsx +1 -1
  29. package/src/flows/flows.tsx +20 -20
  30. package/src/folder-contents/folder-contents.tsx +7 -7
  31. package/src/graph-pane/graph-pane.tsx +1 -1
  32. package/src/hooks/use-capabilities.ts +22 -0
  33. package/src/i18n/en.json +156 -64
  34. package/src/kind-icon.ts +5 -2
  35. package/src/{knowledge-editor/knowledge-editor.tsx → node-editor/node-editor.tsx} +20 -20
  36. package/src/{knowledge-graph → node-graph}/graph-notice.tsx +1 -1
  37. package/src/{knowledge-graph/knowledge-graph.tsx → node-graph/node-graph.tsx} +4 -4
  38. package/src/{knowledge-table/knowledge-table.tsx → node-table/node-table.tsx} +14 -14
  39. package/src/{knowledge/knowledge.tsx → nodes/nodes.tsx} +46 -29
  40. package/src/resource-menu/resource-menu.tsx +105 -62
  41. package/src/router/router.tsx +17 -5
  42. package/src/title-row/title-row.tsx +13 -5
  43. package/vite.config.ts +4 -0
  44. /package/src/{knowledge-graph/knowledge-graph.ts → node-graph/node-graph.ts} +0 -0
@@ -0,0 +1,154 @@
1
+ import type { Flow, Node } from "@anchrd/intel-contract";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { ArchiveRestore } from "lucide-react";
4
+ import { kindIcons } from "@/kind-icon.ts";
5
+ import { useIntelRouterContext } from "@/router/router-context.ts";
6
+
7
+ // One archived thing, whichever side of the tree it came from. The two records stay apart
8
+ // everywhere else (ADR-0004); here they are one list because "what did I throw away" is one
9
+ // question, and an archive split in two would make somebody ask it twice.
10
+ interface ArchivedEntry {
11
+ id: string;
12
+ title: string;
13
+ kind: Node["kind"] | "flow";
14
+ archivedAt: string;
15
+ updatedAt: string;
16
+ restore(): Promise<unknown>;
17
+ }
18
+
19
+ export function Archive() {
20
+ const { data, i18n } = useIntelRouterContext();
21
+ const queryClient = useQueryClient();
22
+
23
+ const archived = useQuery({
24
+ queryKey: ["archive"],
25
+ queryFn: async (): Promise<ArchivedEntry[]> => {
26
+ const [nodes, flows] = await Promise.all([
27
+ data.listNodes({ archivedOnly: true }),
28
+ data.listFlows({ archivedOnly: true }),
29
+ ]);
30
+ const entries: ArchivedEntry[] = [
31
+ ...nodes.items.flatMap((node) =>
32
+ node.archivedAt
33
+ ? [
34
+ {
35
+ id: node.id,
36
+ title: node.title,
37
+ kind: node.kind,
38
+ archivedAt: node.archivedAt,
39
+ updatedAt: node.updatedAt,
40
+ restore: () =>
41
+ data.archiveNode({
42
+ nodeId: node.id,
43
+ baseUpdatedAt: node.updatedAt,
44
+ archived: false,
45
+ idempotencyKey: crypto.randomUUID(),
46
+ }),
47
+ } satisfies ArchivedEntry,
48
+ ]
49
+ : [],
50
+ ),
51
+ ...flows.items.flatMap((flow: Flow) =>
52
+ flow.archivedAt
53
+ ? [
54
+ {
55
+ id: flow.id,
56
+ title: flow.title,
57
+ kind: "flow" as const,
58
+ archivedAt: flow.archivedAt,
59
+ updatedAt: flow.updatedAt,
60
+ restore: () =>
61
+ data.archiveFlow({
62
+ flowId: flow.id,
63
+ baseUpdatedAt: flow.updatedAt,
64
+ archived: false,
65
+ idempotencyKey: crypto.randomUUID(),
66
+ }),
67
+ } satisfies ArchivedEntry,
68
+ ]
69
+ : [],
70
+ ),
71
+ ];
72
+ // Newest first, across both kinds: the thing somebody is looking for is almost always the
73
+ // thing they just archived.
74
+ return entries.sort((a, b) => b.archivedAt.localeCompare(a.archivedAt));
75
+ },
76
+ });
77
+
78
+ // ⚠️ `baseUpdatedAt` from the row that is on screen, exactly as archiving used it. Restoring is
79
+ // a change like any other and takes the same conflict route — without it, two people tidying up
80
+ // at once would silently overwrite each other.
81
+ const restore = useMutation({
82
+ mutationFn: async (entry: ArchivedEntry) => await entry.restore(),
83
+ onSuccess: async () => {
84
+ await Promise.all([
85
+ queryClient.invalidateQueries({ queryKey: ["archive"] }),
86
+ // The tree gets it back, so every level has to be asked again — the entry may sit
87
+ // anywhere, and the archive does not know where.
88
+ queryClient.invalidateQueries({ queryKey: ["tree"] }),
89
+ queryClient.invalidateQueries({ queryKey: ["node-graph"] }),
90
+ queryClient.invalidateQueries({ queryKey: ["flows"] }),
91
+ ]);
92
+ },
93
+ });
94
+
95
+ return (
96
+ <div className="flex min-h-0 flex-1 flex-col">
97
+ <div className="border-b px-6 py-4">
98
+ <h1 className="text-lg font-medium text-foreground">{i18n.t("archive.title")}</h1>
99
+ <p className="mt-1 max-w-prose text-sm text-muted-foreground">
100
+ {i18n.t("archive.description")}
101
+ </p>
102
+ </div>
103
+ <div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">
104
+ {archived.isError ? (
105
+ <p role="alert" className="text-sm text-destructive">
106
+ {i18n.t("archive.failed")}
107
+ </p>
108
+ ) : null}
109
+ {restore.isError ? (
110
+ <p role="alert" className="mb-4 text-sm text-destructive">
111
+ {i18n.t("archive.restoreFailed")}
112
+ </p>
113
+ ) : null}
114
+ {archived.data?.length === 0 ? (
115
+ <p className="text-sm text-muted-foreground">{i18n.t("archive.empty")}</p>
116
+ ) : null}
117
+ <ul className="grid gap-1">
118
+ {(archived.data ?? []).map((entry) => (
119
+ <li
120
+ key={`${entry.kind}-${entry.id}`}
121
+ className="flex items-center gap-3 rounded-md border px-3 py-2"
122
+ >
123
+ {(() => {
124
+ // The same map the tree and the folder table draw from — a second one here would
125
+ // drift the first time a kind is added.
126
+ const Icon = kindIcons[entry.kind];
127
+ return (
128
+ <Icon aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
129
+ );
130
+ })()}
131
+ <span className="sr-only">{i18n.t(`node.kind.${entry.kind}`)}</span>
132
+ <span className="grid min-w-0 flex-1 leading-tight">
133
+ <span className="truncate text-sm font-medium">{entry.title}</span>
134
+ <span className="truncate text-xs text-muted-foreground">
135
+ {i18n.t("archive.archivedAt", { when: entry.archivedAt.slice(0, 10) })}
136
+ </span>
137
+ </span>
138
+ <button
139
+ type="button"
140
+ disabled={restore.isPending}
141
+ onClick={() => restore.mutate(entry)}
142
+ aria-label={i18n.t("archive.restore", { title: entry.title })}
143
+ className="inline-flex h-8 shrink-0 items-center gap-2 rounded-md border px-3 text-sm font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60"
144
+ >
145
+ <ArchiveRestore aria-hidden="true" className="size-4" />
146
+ {i18n.t("archive.restoreAction")}
147
+ </button>
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,39 @@
1
+ import { Avatar as AvatarPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
7
+ return (
8
+ <AvatarPrimitive.Root
9
+ data-slot="avatar"
10
+ className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
17
+ return (
18
+ <AvatarPrimitive.Image
19
+ data-slot="avatar-image"
20
+ className={cn("aspect-square size-full", className)}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AvatarFallback({
27
+ className,
28
+ ...props
29
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
30
+ return (
31
+ <AvatarPrimitive.Fallback
32
+ data-slot="avatar-fallback"
33
+ className={cn("flex size-full items-center justify-center rounded-full bg-muted", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ export { Avatar, AvatarFallback, AvatarImage };
@@ -0,0 +1,163 @@
1
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
2
+ import { Select as SelectPrimitive } from "radix-ui";
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
8
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
9
+ }
10
+
11
+ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
12
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
13
+ }
14
+
15
+ function SelectTrigger({
16
+ className,
17
+ size = "default",
18
+ children,
19
+ ...props
20
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { size?: "sm" | "default" }) {
21
+ return (
22
+ <SelectPrimitive.Trigger
23
+ data-slot="select-trigger"
24
+ data-size={size}
25
+ className={cn(
26
+ "flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 dark:bg-input/30 dark:hover:bg-input/50 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ <SelectPrimitive.Icon asChild>
33
+ {/* ⚠️ The chevron is not decoration: it is what tells a reader this is something to open
34
+ rather than a word beside a label. #143 asks for it explicitly on the role tags. */}
35
+ <ChevronDownIcon className="size-4 opacity-50" />
36
+ </SelectPrimitive.Icon>
37
+ </SelectPrimitive.Trigger>
38
+ );
39
+ }
40
+
41
+ function SelectContent({
42
+ className,
43
+ children,
44
+ position = "popper",
45
+ ...props
46
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
47
+ return (
48
+ <SelectPrimitive.Portal>
49
+ <SelectPrimitive.Content
50
+ data-slot="select-content"
51
+ className={cn(
52
+ "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
53
+ position === "popper" &&
54
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
55
+ className,
56
+ )}
57
+ position={position}
58
+ {...props}
59
+ >
60
+ <SelectScrollUpButton />
61
+ <SelectPrimitive.Viewport
62
+ className={cn(
63
+ "p-1",
64
+ position === "popper" &&
65
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
66
+ )}
67
+ >
68
+ {children}
69
+ </SelectPrimitive.Viewport>
70
+ <SelectScrollDownButton />
71
+ </SelectPrimitive.Content>
72
+ </SelectPrimitive.Portal>
73
+ );
74
+ }
75
+
76
+ function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
77
+ return (
78
+ <SelectPrimitive.Label
79
+ data-slot="select-label"
80
+ className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
81
+ {...props}
82
+ />
83
+ );
84
+ }
85
+
86
+ function SelectItem({
87
+ className,
88
+ children,
89
+ ...props
90
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
91
+ return (
92
+ <SelectPrimitive.Item
93
+ data-slot="select-item"
94
+ className={cn(
95
+ "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
96
+ className,
97
+ )}
98
+ {...props}
99
+ >
100
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
101
+ <SelectPrimitive.ItemIndicator>
102
+ <CheckIcon className="size-4" />
103
+ </SelectPrimitive.ItemIndicator>
104
+ </span>
105
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
106
+ </SelectPrimitive.Item>
107
+ );
108
+ }
109
+
110
+ function SelectSeparator({
111
+ className,
112
+ ...props
113
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
114
+ return (
115
+ <SelectPrimitive.Separator
116
+ data-slot="select-separator"
117
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
118
+ {...props}
119
+ />
120
+ );
121
+ }
122
+
123
+ function SelectScrollUpButton({
124
+ className,
125
+ ...props
126
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
127
+ return (
128
+ <SelectPrimitive.ScrollUpButton
129
+ data-slot="select-scroll-up-button"
130
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
131
+ {...props}
132
+ >
133
+ <ChevronUpIcon className="size-4" />
134
+ </SelectPrimitive.ScrollUpButton>
135
+ );
136
+ }
137
+
138
+ function SelectScrollDownButton({
139
+ className,
140
+ ...props
141
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
142
+ return (
143
+ <SelectPrimitive.ScrollDownButton
144
+ data-slot="select-scroll-down-button"
145
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
146
+ {...props}
147
+ >
148
+ <ChevronDownIcon className="size-4" />
149
+ </SelectPrimitive.ScrollDownButton>
150
+ );
151
+ }
152
+
153
+ export {
154
+ Select,
155
+ SelectContent,
156
+ SelectItem,
157
+ SelectLabel,
158
+ SelectScrollDownButton,
159
+ SelectScrollUpButton,
160
+ SelectSeparator,
161
+ SelectTrigger,
162
+ SelectValue,
163
+ };
@@ -0,0 +1,52 @@
1
+ import { Tabs as TabsPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
7
+ return (
8
+ <TabsPrimitive.Root
9
+ data-slot="tabs"
10
+ className={cn("flex flex-col gap-2", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
17
+ return (
18
+ <TabsPrimitive.List
19
+ data-slot="tabs-list"
20
+ className={cn(
21
+ "inline-flex w-fit items-center justify-center rounded-lg bg-muted p-[3px] text-muted-foreground",
22
+ className,
23
+ )}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
30
+ return (
31
+ <TabsPrimitive.Trigger
32
+ data-slot="tabs-trigger"
33
+ className={cn(
34
+ "inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground transition-[color,box-shadow] focus-visible:border-ring focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:shadow-sm dark:text-muted-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
43
+ return (
44
+ <TabsPrimitive.Content
45
+ data-slot="tabs-content"
46
+ className={cn("flex-1 outline-none", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ export { Tabs, TabsContent, TabsList, TabsTrigger };
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * What the agent runtime answers, read back defensively.
5
+ *
6
+ * ⚠️ These shapes are NOT in `@anchrd/intel-contract`, and that is deliberate rather than an
7
+ * oversight: the runtime (`packages/agent`) imports nothing from the contract either, so a run
8
+ * record has no shared declaration anywhere. It crosses a network boundary into this package, so it
9
+ * is parsed here — tolerantly, in the same spirit the runtime reads a definition, because a field
10
+ * the runtime adds must not blank the log screen.
11
+ */
12
+ export const AgentRunTarget = z.object({
13
+ kind: z.enum(["chat", "document", "flow", "mail", "task"]),
14
+ id: z.string().nullable(),
15
+ });
16
+ export type AgentRunTarget = z.infer<typeof AgentRunTarget>;
17
+
18
+ export const AgentRun = z.object({
19
+ id: z.string(),
20
+ trigger: z.enum(["chat", "schedule", "mail", "task", "manual"]),
21
+ target: AgentRunTarget,
22
+ status: z.enum(["running", "completed", "failed"]),
23
+ startedAt: z.string(),
24
+ finishedAt: z.string().nullable(),
25
+ steps: z.number(),
26
+ error: z.string().nullable(),
27
+ });
28
+ export type AgentRun = z.infer<typeof AgentRun>;
29
+
30
+ /** Whether the agent is switched off. Runtime state, never part of the definition (#179). */
31
+ export const AgentState = z.object({
32
+ paused: z.boolean(),
33
+ pausedAt: z.string().nullable(),
34
+ alarmAt: z.number().nullable(),
35
+ });
36
+ export type AgentState = z.infer<typeof AgentState>;
37
+
38
+ /** What "run now" answers: the run exists, and it is on its way. */
39
+ export const AgentManualRun = z.object({
40
+ runId: z.string(),
41
+ status: z.string(),
42
+ });
43
+ export type AgentManualRun = z.infer<typeof AgentManualRun>;
44
+
45
+ export const AgentLogEntry = z.object({
46
+ id: z.string(),
47
+ runId: z.string(),
48
+ at: z.string(),
49
+ level: z.enum(["info", "warn", "error"]),
50
+ event: z.string(),
51
+ detail: z.string(),
52
+ });
53
+ export type AgentLogEntry = z.infer<typeof AgentLogEntry>;
54
+
55
+ export const AgentRunList = z.object({ items: z.array(AgentRun) });
56
+ export type AgentRunList = z.infer<typeof AgentRunList>;
57
+
58
+ export const AgentRunDetail = z.object({ run: AgentRun, log: z.array(AgentLogEntry) });
59
+ export type AgentRunDetail = z.infer<typeof AgentRunDetail>;
60
+
61
+ /**
62
+ * The runtime's own address, for the one thing that is dialled from outside this page.
63
+ *
64
+ * ⚠️ Only the agent's MCP endpoint is reached here, and only as a string somebody copies. A portal
65
+ * or another agent dials it directly with its own credential (ADR-0005 §7), so it has to be the
66
+ * runtime's real hostname — a deployment that puts the runtime somewhere else sets
67
+ * `VITE_AGENT_RUNTIME_URL` at build time. Empty means "the origin this page came from".
68
+ *
69
+ * ⚠️ NOT the browser's path any more (#178). Everything this page fetches goes through intel, which
70
+ * proxies it to the runtime over a service binding; see `agentRuntimePath` below.
71
+ */
72
+ export function agentRuntimeBaseUrl(): string {
73
+ return (import.meta.env.VITE_AGENT_RUNTIME_URL ?? "").replace(/\/$/, "");
74
+ }
75
+
76
+ export function agentMcpAddress(agentId: string): string {
77
+ return `${agentRuntimeBaseUrl()}/agents/${encodeURIComponent(agentId)}/mcp`;
78
+ }
79
+
80
+ /**
81
+ * Where the browser asks, which is intel and nothing else.
82
+ *
83
+ * ⚠️ One door, one origin, one credential: the page carries its intel session cookie, intel opens
84
+ * it, and intel forwards the call to the runtime with the Gate token of the person who asked. That
85
+ * is why there is no CORS here, no second hostname to configure, and above all no Gate token in
86
+ * browser JavaScript — the runtime still demands `agents/run`, it just hears it from intel.
87
+ *
88
+ * The path is relative on purpose. It is joined to intel's `/api/v1` by the data provider, which is
89
+ * the only module that knows where intel is.
90
+ */
91
+ export function agentRuntimePath(agentId: string, route: string): string {
92
+ return `/agents/${encodeURIComponent(agentId)}${route}`;
93
+ }