@anchrd/intel-ui 0.3.0 → 0.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 (52) hide show
  1. package/components.json +7 -1
  2. package/package.json +3 -1
  3. package/src/app/action-slot/action-slot.tsx +23 -0
  4. package/src/app/app-sidebar/app-sidebar.tsx +71 -0
  5. package/src/app/app-tree/app-tree.tsx +798 -0
  6. package/src/app/app.tsx +99 -54
  7. package/src/app/header-search/header-search.tsx +291 -0
  8. package/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  9. package/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  10. package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  11. package/src/app/tree-move/tree-move.tsx +197 -0
  12. package/src/app/user-footer/user-footer.tsx +103 -0
  13. package/src/app/view-toggle/view-toggle.tsx +77 -0
  14. package/src/blocknote-view/blocknote-view.tsx +19 -2
  15. package/src/branding/favicon.default.svg +2 -2
  16. package/src/branding/favicon.svg +2 -2
  17. package/src/components/ui/breadcrumb.tsx +102 -0
  18. package/src/components/ui/button.tsx +64 -0
  19. package/src/components/ui/collapsible.tsx +20 -0
  20. package/src/components/ui/command.tsx +160 -0
  21. package/src/components/ui/dialog.tsx +143 -0
  22. package/src/components/ui/dropdown-menu.tsx +162 -0
  23. package/src/components/ui/input.tsx +21 -0
  24. package/src/components/ui/separator.tsx +26 -0
  25. package/src/components/ui/sheet.tsx +136 -0
  26. package/src/components/ui/sidebar.tsx +693 -0
  27. package/src/components/ui/skeleton.tsx +13 -0
  28. package/src/components/ui/tooltip.tsx +51 -0
  29. package/src/data/intel-data-provider/intel-data-provider.ts +170 -85
  30. package/src/data/intel-data-provider/intel-data-provider.types.ts +64 -20
  31. package/src/document-link/document-link.tsx +132 -0
  32. package/src/flow-runs/flow-runs.tsx +225 -0
  33. package/src/flows/flows.tsx +684 -368
  34. package/src/flows/node-icon/node-icon.ts +28 -0
  35. package/src/flows/node-palette/node-palette.tsx +174 -0
  36. package/src/flows/node-palette/node-palette.types.ts +15 -0
  37. package/src/graph-pane/graph-pane.tsx +44 -0
  38. package/src/hooks/use-mobile.ts +19 -0
  39. package/src/i18n/en.json +188 -52
  40. package/src/knowledge/knowledge.tsx +133 -709
  41. package/src/knowledge-editor/knowledge-editor.tsx +169 -21
  42. package/src/knowledge-graph/knowledge-graph.ts +26 -24
  43. package/src/knowledge-graph/knowledge-graph.tsx +33 -24
  44. package/src/knowledge-table/knowledge-table.tsx +129 -0
  45. package/src/main.tsx +2 -2
  46. package/src/resource-menu/resource-menu.tsx +580 -0
  47. package/src/router/router.tsx +4 -0
  48. package/src/router/selection-search.ts +43 -0
  49. package/src/save-button/save-button.tsx +103 -0
  50. package/src/styles.css +91 -51
  51. package/src/theme/theme.ts +24 -0
  52. package/src/tools/tools.tsx +175 -158
@@ -0,0 +1,51 @@
1
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delayDuration={delayDuration}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
20
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
21
+ }
22
+
23
+ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
24
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
25
+ }
26
+
27
+ function TooltipContent({
28
+ className,
29
+ sideOffset = 0,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
33
+ return (
34
+ <TooltipPrimitive.Portal>
35
+ <TooltipPrimitive.Content
36
+ data-slot="tooltip-content"
37
+ sideOffset={sideOffset}
38
+ className={cn(
39
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
40
+ className,
41
+ )}
42
+ {...props}
43
+ >
44
+ {children}
45
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
46
+ </TooltipPrimitive.Content>
47
+ </TooltipPrimitive.Portal>
48
+ );
49
+ }
50
+
51
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
@@ -1,27 +1,37 @@
1
1
  import {
2
+ AppendKnowledgeTableRowsInput,
3
+ AppendKnowledgeTableRowsResult,
2
4
  ArchiveKnowledgeNodeInput,
3
5
  CompleteFlowRunStepInput,
4
6
  CreateFlowInput,
5
- CreateKnowledgeLinkInput,
6
7
  CreateKnowledgeNodeInput,
7
- DeleteKnowledgeLinkInput,
8
+ DefineKnowledgeTableInput,
8
9
  Flow,
9
10
  FlowDocument,
10
11
  FlowList,
12
+ FlowPublishPreview,
13
+ FlowRequirements,
14
+ FlowRunHistory,
15
+ FlowRunList,
11
16
  FlowRunStep,
12
17
  KnowledgeDocument,
13
18
  KnowledgeGraph,
14
- KnowledgeLink,
15
19
  KnowledgeLinkList,
16
20
  KnowledgeNode,
17
21
  KnowledgeNodeList,
22
+ KnowledgeTable,
18
23
  KnowledgeVersionList,
24
+ ListFlowRunsInput,
25
+ ListFlowsInput,
19
26
  ListKnowledgeNodesInput,
27
+ PreviewFlowPublishInput,
20
28
  ProblemDetails,
21
29
  PublishFlowInput,
22
- ResourceGrant,
30
+ RelationGraph,
31
+ RelationGraphInput,
32
+ ResolveKnowledgeLinksInput,
33
+ ResolveKnowledgeLinksResult,
23
34
  ResourceGrantList,
24
- RevokeFlowGrantInput,
25
35
  RevokeGrantResult,
26
36
  RevokeKnowledgeGrantInput,
27
37
  SaveFlowVersionInput,
@@ -29,31 +39,50 @@ import {
29
39
  SaveKnowledgeVersionInput,
30
40
  SearchKnowledgeInput,
31
41
  SearchKnowledgeResult,
32
- ShareFlowInput,
42
+ SessionUser,
33
43
  ShareKnowledgeInput,
44
+ ShareKnowledgeResult,
34
45
  StartFlowRunInput,
35
- TestToolInput,
36
46
  ToolCatalog,
37
- ToolTestResult,
47
+ UpdateFlowInput,
38
48
  UpdateKnowledgeNodeInput,
39
49
  } from "@anchrd/intel-contract";
40
- import { z } from "zod";
41
- import type { IntelDataProvider, KnowledgeTreeNode } from "./intel-data-provider.types.ts";
50
+ import type { z } from "zod";
51
+ import type { IntelDataProvider, TreeEntry } from "./intel-data-provider.types.ts";
42
52
 
43
- const DeleteResult = z.strictObject({ deleted: z.boolean() });
53
+ // ⚠️ The refusal's `code`, not only its prose. A view that has to tell four different refusals
54
+ // apart — "you may not write there", "that is not a folder", "that would be a cycle", "someone
55
+ // else changed it" — cannot do so from `detail`, which is a sentence the server is free to
56
+ // reword. The code is the contract (`ProblemDetails.code`); the message stays what it was.
57
+ export class IntelRequestError extends Error {
58
+ constructor(
59
+ public readonly status: number,
60
+ public readonly code: string | null,
61
+ message: string,
62
+ ) {
63
+ super(message);
64
+ this.name = "IntelRequestError";
65
+ }
66
+ }
44
67
 
45
68
  // Attachment uploads and downloads share this budget, so it is generous rather than snappy.
46
69
  const RequestTimeoutMs = 60_000;
47
70
 
48
- // Deeper nesting than this is not a tree anyone navigates; the limit exists to bound the request
49
- // fan-out on first paint, not to express a product rule.
50
- const MaxTreeDepth = 12;
51
-
52
71
  // The Worker serves the login route, so the path belongs to the data layer rather than to any view.
53
72
  export function loginPath(returnTo?: string): string {
54
73
  return returnTo ? `/auth/login?returnTo=${encodeURIComponent(returnTo)}` : "/auth/login";
55
74
  }
56
75
 
76
+ /**
77
+ * One flow as one row of the tree. Two levels show flows — the contents of a folder, and what an
78
+ * expanded flow calls — and they are two lists of the same rows, so they are built here once. Two
79
+ * copies of this would keep returning something plausible and slowly stop agreeing on what a flow
80
+ * row is (#30).
81
+ */
82
+ export function flowEntry(flow: Flow): TreeEntry {
83
+ return { type: "flow", id: flow.id, title: flow.title, kind: "flow", flow };
84
+ }
85
+
57
86
  export function createIntelDataProvider(
58
87
  deps: { fetch?: typeof fetch; baseUrl?: string; onUnauthorized?(): void } = {},
59
88
  ): IntelDataProvider {
@@ -89,7 +118,9 @@ export function createIntelDataProvider(
89
118
  .json()
90
119
  .catch(() => null),
91
120
  );
92
- throw new Error(
121
+ throw new IntelRequestError(
122
+ result.status,
123
+ parsed.success ? (parsed.data.code ?? null) : null,
93
124
  parsed.success
94
125
  ? (parsed.data.detail ?? parsed.data.title)
95
126
  : `Intel responded with ${result.status}`,
@@ -106,6 +137,17 @@ export function createIntelDataProvider(
106
137
  return schema.parse(await (await response(path, init)).json());
107
138
  }
108
139
 
140
+ async function listFlows(input: ListFlowsInput = {}): Promise<z.infer<typeof FlowList>> {
141
+ const query = ListFlowsInput.parse(input);
142
+ // The parameter is only sent when the caller asked for one folder. Sending an empty one for
143
+ // "everything" would ask the server for the root instead.
144
+ const params = new URLSearchParams(
145
+ query.parentId === undefined ? {} : { parentId: query.parentId ?? "" },
146
+ );
147
+ const search = params.toString();
148
+ return await request(`/flows${search ? `?${search}` : ""}`, FlowList);
149
+ }
150
+
109
151
  async function listKnowledge(
110
152
  input: Partial<ListKnowledgeNodesInput> = {},
111
153
  ): Promise<z.infer<typeof KnowledgeNodeList>> {
@@ -115,31 +157,37 @@ export function createIntelDataProvider(
115
157
  return await request(`/knowledge?${params}`, KnowledgeNodeList);
116
158
  }
117
159
 
118
- // One request per folder, so a deep tree is an N+1 on the first paint. The depth limit bounds that
119
- // and, more importantly, makes a cyclic parentId impossible to hang on: without it a cycle would
120
- // recurse until the tab dies. `seen` stops a cycle at the first repeat rather than at the limit.
121
- async function loadChildren(
122
- parentId: string | null,
123
- depth = 0,
124
- seen: ReadonlySet<string> = new Set(),
125
- ): Promise<KnowledgeTreeNode[]> {
126
- if (depth >= MaxTreeDepth) return [];
127
- const { items } = await listKnowledge({ parentId });
128
- return await Promise.all(
129
- items.map(async (node) => ({
130
- ...node,
131
- children:
132
- node.kind === "folder" && !seen.has(node.id)
133
- ? await loadChildren(node.id, depth + 1, new Set([...seen, node.id]))
134
- : [],
135
- })),
136
- );
137
- }
138
-
139
160
  return {
161
+ async getSession() {
162
+ return await request("/session", SessionUser);
163
+ },
140
164
  listKnowledge,
141
- async loadKnowledgeTree() {
142
- return await loadChildren(null);
165
+ async listTreeChildren(parentId) {
166
+ // Both sides of one folder, asked for in parallel and merged here rather than on the server:
167
+ // the folder is shared, the two records are not, and no endpoint may return a row that is a
168
+ // bit of both (ADR-0004).
169
+ const [knowledge, flows] = await Promise.all([
170
+ listKnowledge({ parentId }),
171
+ listFlows({ parentId }),
172
+ ]);
173
+ const entries: TreeEntry[] = [
174
+ ...knowledge.items.map((node) => ({
175
+ type: "knowledge" as const,
176
+ id: node.id,
177
+ title: node.title,
178
+ kind: node.kind,
179
+ node,
180
+ })),
181
+ ...flows.items.map(flowEntry),
182
+ ];
183
+ // Folders first, then everything else by title: a document and a flow sit side by side, and
184
+ // the icon is what tells them apart.
185
+ return entries.sort(
186
+ (left, right) =>
187
+ Number(right.kind === "folder") - Number(left.kind === "folder") ||
188
+ left.title.localeCompare(right.title) ||
189
+ left.id.localeCompare(right.id),
190
+ );
143
191
  },
144
192
  async getKnowledge(nodeId) {
145
193
  return await request(`/knowledge/${encodeURIComponent(nodeId)}`, KnowledgeDocument);
@@ -156,21 +204,13 @@ export function createIntelDataProvider(
156
204
  async listKnowledgeLinks(nodeId) {
157
205
  return await request(`/knowledge/${encodeURIComponent(nodeId)}/links`, KnowledgeLinkList);
158
206
  },
159
- async createKnowledgeLink(input) {
160
- const parsed = CreateKnowledgeLinkInput.parse(input);
161
- return await request(
162
- `/knowledge/${encodeURIComponent(parsed.sourceNodeId)}/links`,
163
- KnowledgeLink,
164
- { method: "POST", body: JSON.stringify(parsed) },
165
- );
166
- },
167
- async deleteKnowledgeLink(input) {
168
- const parsed = DeleteKnowledgeLinkInput.parse(input);
169
- return await request(
170
- `/knowledge/${encodeURIComponent(parsed.sourceNodeId)}/links/${encodeURIComponent(parsed.linkId)}/revoke`,
171
- DeleteResult,
172
- { method: "POST", body: JSON.stringify(parsed) },
173
- );
207
+ // ⚠️ POST, not GET. The identifiers of the documents a text links to belong in a body: a
208
+ // query string of them would end up in logs and referrers.
209
+ async resolveKnowledgeLinks(input) {
210
+ return await request("/knowledge/links/resolve", ResolveKnowledgeLinksResult, {
211
+ method: "POST",
212
+ body: JSON.stringify(ResolveKnowledgeLinksInput.parse(input)),
213
+ });
174
214
  },
175
215
  async saveKnowledge(input) {
176
216
  const parsed = SaveKnowledgeVersionInput.parse(input);
@@ -191,6 +231,28 @@ export function createIntelDataProvider(
191
231
  async getKnowledgeAttachment(nodeId) {
192
232
  return await (await response(`/knowledge/${encodeURIComponent(nodeId)}/attachment`)).blob();
193
233
  },
234
+ async getKnowledgeTable(nodeId) {
235
+ return await request(`/knowledge/${encodeURIComponent(nodeId)}/table`, KnowledgeTable);
236
+ },
237
+ async defineKnowledgeTable(input) {
238
+ const parsed = DefineKnowledgeTableInput.parse(input);
239
+ return await request(
240
+ `/knowledge/${encodeURIComponent(parsed.nodeId)}/table`,
241
+ KnowledgeTable,
242
+ {
243
+ method: "POST",
244
+ body: JSON.stringify(parsed),
245
+ },
246
+ );
247
+ },
248
+ async appendKnowledgeTableRows(input) {
249
+ const parsed = AppendKnowledgeTableRowsInput.parse(input);
250
+ return await request(
251
+ `/knowledge/${encodeURIComponent(parsed.nodeId)}/table/rows`,
252
+ AppendKnowledgeTableRowsResult,
253
+ { method: "POST", body: JSON.stringify(parsed) },
254
+ );
255
+ },
194
256
  async listKnowledgeVersions(nodeId) {
195
257
  return await request(
196
258
  `/knowledge/${encodeURIComponent(nodeId)}/versions`,
@@ -228,7 +290,7 @@ export function createIntelDataProvider(
228
290
  const parsed = ShareKnowledgeInput.parse(input);
229
291
  return await request(
230
292
  `/knowledge/${encodeURIComponent(parsed.resourceId)}/grants`,
231
- ResourceGrant,
293
+ ShareKnowledgeResult,
232
294
  { method: "POST", body: JSON.stringify(parsed) },
233
295
  );
234
296
  },
@@ -240,39 +302,46 @@ export function createIntelDataProvider(
240
302
  { method: "POST", body: JSON.stringify(parsed) },
241
303
  );
242
304
  },
243
- async listFlows() {
244
- return await request("/flows", FlowList);
245
- },
305
+ listFlows,
246
306
  async getFlow(flowId) {
247
307
  return await request(`/flows/${encodeURIComponent(flowId)}`, FlowDocument);
248
308
  },
309
+ async listFlowCalls(flowId) {
310
+ return await request(`/flows/${encodeURIComponent(flowId)}/calls`, FlowList);
311
+ },
312
+ async getRelationGraph(scope, limit) {
313
+ const parsed = RelationGraphInput.parse({
314
+ scope,
315
+ ...(limit === undefined ? {} : { limit }),
316
+ });
317
+ const params = new URLSearchParams({
318
+ of: parsed.scope.of,
319
+ limit: String(parsed.limit),
320
+ // A folder scope without an ID is the root of the shared tree, and an absent parameter is
321
+ // how that is said — the same distinction `/flows?parentId=` makes.
322
+ ...(parsed.scope.of === "flow"
323
+ ? { flowId: parsed.scope.flowId }
324
+ : parsed.scope.folderId
325
+ ? { folderId: parsed.scope.folderId }
326
+ : {}),
327
+ });
328
+ return await request(`/flows/graph?${params}`, RelationGraph);
329
+ },
330
+ async getFlowRequirements(flowId) {
331
+ return await request(`/flows/${encodeURIComponent(flowId)}/requirements`, FlowRequirements);
332
+ },
249
333
  async createFlow(input) {
250
334
  return await request("/flows", Flow, {
251
335
  method: "POST",
252
336
  body: JSON.stringify(CreateFlowInput.parse(input)),
253
337
  });
254
338
  },
255
- async listFlowGrants(flowId) {
256
- return await request(`/flows/${encodeURIComponent(flowId)}/grants`, ResourceGrantList);
257
- },
258
- async shareFlow(input) {
259
- const parsed = ShareFlowInput.parse(input);
260
- return await request(
261
- `/flows/${encodeURIComponent(parsed.resourceId)}/grants`,
262
- ResourceGrant,
263
- {
264
- method: "POST",
265
- body: JSON.stringify(parsed),
266
- },
267
- );
268
- },
269
- async revokeFlowGrant(input) {
270
- const parsed = RevokeFlowGrantInput.parse(input);
271
- return await request(
272
- `/flows/${encodeURIComponent(parsed.resourceId)}/grants/${encodeURIComponent(parsed.grantId)}/revoke`,
273
- RevokeGrantResult,
274
- { method: "POST", body: JSON.stringify(parsed) },
275
- );
339
+ async updateFlow(input) {
340
+ const parsed = UpdateFlowInput.parse(input);
341
+ return await request(`/flows/${encodeURIComponent(parsed.flowId)}`, Flow, {
342
+ method: "PATCH",
343
+ body: JSON.stringify(parsed),
344
+ });
276
345
  },
277
346
  async saveFlow(input) {
278
347
  const parsed = SaveFlowVersionInput.parse(input);
@@ -281,6 +350,13 @@ export function createIntelDataProvider(
281
350
  body: JSON.stringify(parsed),
282
351
  });
283
352
  },
353
+ async previewFlowPublish(input) {
354
+ const parsed = PreviewFlowPublishInput.parse(input);
355
+ return await request(
356
+ `/flows/${encodeURIComponent(parsed.flowId)}/versions/${encodeURIComponent(parsed.versionId)}/publish-preview`,
357
+ FlowPublishPreview,
358
+ );
359
+ },
284
360
  async publishFlow(input) {
285
361
  const parsed = PublishFlowInput.parse(input);
286
362
  return await request(`/flows/${encodeURIComponent(parsed.flowId)}/publish`, Flow, {
@@ -295,9 +371,24 @@ export function createIntelDataProvider(
295
371
  body: JSON.stringify(parsed),
296
372
  });
297
373
  },
374
+ async listFlowRuns(input) {
375
+ const parsed = ListFlowRunsInput.parse(input);
376
+ // Only what was actually asked for travels. `failedOnly` is the parameter's presence rather
377
+ // than a value, which is the same shape the server reads it back with.
378
+ const params = new URLSearchParams({ limit: String(parsed.limit) });
379
+ if (parsed.failedOnly) params.set("failedOnly", "");
380
+ if (parsed.cursor) params.set("cursor", parsed.cursor);
381
+ return await request(
382
+ `/flows/${encodeURIComponent(parsed.flowId)}/runs?${params}`,
383
+ FlowRunList,
384
+ );
385
+ },
298
386
  async getFlowRun(runId) {
299
387
  return await request(`/flow-runs/${encodeURIComponent(runId)}`, FlowRunStep);
300
388
  },
389
+ async getFlowRunSteps(runId) {
390
+ return await request(`/flow-runs/${encodeURIComponent(runId)}/steps`, FlowRunHistory);
391
+ },
301
392
  async completeFlowStep(input) {
302
393
  const parsed = CompleteFlowRunStepInput.parse(input);
303
394
  return await request(`/flow-runs/${encodeURIComponent(parsed.runId)}/complete`, FlowRunStep, {
@@ -308,12 +399,6 @@ export function createIntelDataProvider(
308
399
  async listTools() {
309
400
  return await request("/tools", ToolCatalog);
310
401
  },
311
- async testTool(input) {
312
- return await request("/tools/test", ToolTestResult, {
313
- method: "POST",
314
- body: JSON.stringify(TestToolInput.parse(input)),
315
- });
316
- },
317
402
 
318
403
  portalConnectUrl(returnTo = "/tools") {
319
404
  return `${baseUrl}/auth/connect?returnTo=${encodeURIComponent(returnTo)}`;
@@ -1,26 +1,37 @@
1
1
  import type {
2
+ AppendKnowledgeTableRowsInput,
3
+ AppendKnowledgeTableRowsResult,
2
4
  ArchiveKnowledgeNodeInput,
3
5
  CompleteFlowRunStepInput,
4
6
  CreateFlowInput,
5
- CreateKnowledgeLinkInput,
6
7
  CreateKnowledgeNodeInput,
7
- DeleteKnowledgeLinkInput,
8
+ DefineKnowledgeTableInput,
8
9
  Flow,
9
10
  FlowDocument,
10
11
  FlowList,
12
+ FlowPublishPreview,
13
+ FlowRequirements,
14
+ FlowRunHistory,
15
+ FlowRunList,
11
16
  FlowRunStep,
12
17
  KnowledgeDocument,
13
18
  KnowledgeGraph,
14
- KnowledgeLink,
15
19
  KnowledgeLinkList,
16
20
  KnowledgeNode,
21
+ KnowledgeNodeKind,
17
22
  KnowledgeNodeList,
23
+ KnowledgeTable,
18
24
  KnowledgeVersionList,
25
+ ListFlowRunsInput,
26
+ ListFlowsInput,
19
27
  ListKnowledgeNodesInput,
28
+ PreviewFlowPublishInput,
20
29
  PublishFlowInput,
21
- ResourceGrant,
30
+ RelationGraph,
31
+ RelationGraphScope,
32
+ ResolveKnowledgeLinksInput,
33
+ ResolveKnowledgeLinksResult,
22
34
  ResourceGrantList,
23
- RevokeFlowGrantInput,
24
35
  RevokeGrantResult,
25
36
  RevokeKnowledgeGrantInput,
26
37
  SaveFlowVersionInput,
@@ -28,51 +39,84 @@ import type {
28
39
  SaveKnowledgeVersionInput,
29
40
  SearchKnowledgeInput,
30
41
  SearchKnowledgeResult,
31
- ShareFlowInput,
42
+ SessionUser,
32
43
  ShareKnowledgeInput,
44
+ ShareKnowledgeResult,
33
45
  StartFlowRunInput,
34
- TestToolInput,
35
46
  ToolCatalog,
36
- ToolTestResult,
47
+ UpdateFlowInput,
37
48
  UpdateKnowledgeNodeInput,
38
49
  } from "@anchrd/intel-contract";
39
50
 
40
- export interface KnowledgeTreeNode extends KnowledgeNode {
41
- children: KnowledgeTreeNode[];
42
- }
51
+ // One row of the shared tree. Knowledge and Flows share the folder, not their nature (ADR-0004), so
52
+ // this is a union that keeps each side's record whole — never a merged "node" that is a bit of both.
53
+ export type TreeEntry =
54
+ | { type: "knowledge"; id: string; title: string; kind: KnowledgeNodeKind; node: KnowledgeNode }
55
+ | { type: "flow"; id: string; title: string; kind: "flow"; flow: Flow };
43
56
 
44
57
  export interface IntelDataProvider {
58
+ getSession(): Promise<SessionUser>;
45
59
  listKnowledge(input?: Partial<ListKnowledgeNodesInput>): Promise<KnowledgeNodeList>;
46
- loadKnowledgeTree(): Promise<KnowledgeTreeNode[]>;
60
+ // One level of the shared tree: the documents and the flows filed in the same folder, in one
61
+ // sorted list. Per level rather than recursive, so opening a folder is what costs a request.
62
+ listTreeChildren(parentId: string | null): Promise<TreeEntry[]>;
47
63
  getKnowledge(nodeId: string): Promise<KnowledgeDocument>;
48
64
  createKnowledge(input: CreateKnowledgeNodeInput): Promise<KnowledgeNode>;
49
65
  getKnowledgeGraph(limit?: number): Promise<KnowledgeGraph>;
50
66
  listKnowledgeLinks(nodeId: string): Promise<KnowledgeLinkList>;
51
- createKnowledgeLink(input: CreateKnowledgeLinkInput): Promise<KnowledgeLink>;
52
- deleteKnowledgeLink(input: DeleteKnowledgeLinkInput): Promise<{ deleted: boolean }>;
67
+ // The titles of the documents a text links to, for the reader. A target they may not see, or one
68
+ // that is gone, is absent from the answer — the two look the same on purpose (#41).
69
+ resolveKnowledgeLinks(input: ResolveKnowledgeLinksInput): Promise<ResolveKnowledgeLinksResult>;
53
70
  saveKnowledge(input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
54
71
  saveKnowledgeAttachment(input: SaveKnowledgeAttachmentInput): Promise<KnowledgeDocument>;
55
72
  getKnowledgeAttachment(nodeId: string): Promise<Blob>;
73
+ // A table as columns and rows. The CSV stays canonical — `getKnowledge` still answers with it,
74
+ // which is what the download uses — and this is the same bytes read by the server's one reader.
75
+ getKnowledgeTable(nodeId: string): Promise<KnowledgeTable>;
76
+ defineKnowledgeTable(input: DefineKnowledgeTableInput): Promise<KnowledgeTable>;
77
+ appendKnowledgeTableRows(
78
+ input: AppendKnowledgeTableRowsInput,
79
+ ): Promise<AppendKnowledgeTableRowsResult>;
56
80
  portalConnectUrl(returnTo?: string): string;
57
81
  listKnowledgeVersions(nodeId: string): Promise<KnowledgeVersionList>;
58
82
  updateKnowledge(input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
59
83
  archiveKnowledge(input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
60
84
  searchKnowledge(input: SearchKnowledgeInput): Promise<SearchKnowledgeResult>;
61
85
  listKnowledgeGrants(resourceId: string): Promise<ResourceGrantList>;
62
- shareKnowledge(input: ShareKnowledgeInput): Promise<ResourceGrant>;
86
+ // The grant, and what the grant does not cover: the documents the flows in this folder read that
87
+ // the new principal still cannot. A warning, never a refusal (ADR-0004 §4).
88
+ shareKnowledge(input: ShareKnowledgeInput): Promise<ShareKnowledgeResult>;
63
89
  revokeKnowledgeGrant(input: RevokeKnowledgeGrantInput): Promise<RevokeGrantResult>;
64
- listFlows(): Promise<FlowList>;
90
+ listFlows(input?: ListFlowsInput): Promise<FlowList>;
65
91
  getFlow(flowId: string): Promise<FlowDocument>;
92
+ // What a flow calls, read out of its graph. It answers a different question from `listTreeChildren`
93
+ // and deliberately gives a different answer: a shared sub-flow is listed under every caller.
94
+ listFlowCalls(flowId: string): Promise<FlowList>;
95
+ // What accesses what, for one level of the shared tree: a folder for its contents, a flow for
96
+ // itself. ⚠️ Only nodes the signed-in user may see come back, and one they may not is absent
97
+ // altogether — never a placeholder, because the edge into one would already say that it exists.
98
+ getRelationGraph(scope: RelationGraphScope, limit?: number): Promise<RelationGraph>;
99
+ // What a flow touches: the documents and tools its graph names. Documents the signed-in user may
100
+ // not see are counted rather than named, and nothing here claims anybody may reach them.
101
+ getFlowRequirements(flowId: string): Promise<FlowRequirements>;
66
102
  createFlow(input: CreateFlowInput): Promise<Flow>;
67
- listFlowGrants(flowId: string): Promise<ResourceGrantList>;
68
- shareFlow(input: ShareFlowInput): Promise<ResourceGrant>;
69
- revokeFlowGrant(input: RevokeFlowGrantInput): Promise<RevokeGrantResult>;
103
+ updateFlow(input: UpdateFlowInput): Promise<Flow>;
70
104
  saveFlow(input: SaveFlowVersionInput): Promise<FlowDocument>;
105
+ // Which version each sub-flow call will take once published, and which of them publishing
106
+ // freezes. Read before publishing, so the author agrees to the pins rather than discovering them.
107
+ previewFlowPublish(input: PreviewFlowPublishInput): Promise<FlowPublishPreview>;
71
108
  publishFlow(input: PublishFlowInput): Promise<Flow>;
72
109
  startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
110
+ // What this flow has done, newest first, one page at a time. ⚠️ It carries what a run did and
111
+ // never what it produced: a run reads Knowledge with the rights of whoever started it, so its
112
+ // result is not automatically readable for everyone who may read the flow.
113
+ listFlowRuns(input: ListFlowRunsInput): Promise<FlowRunList>;
73
114
  getFlowRun(runId: string): Promise<FlowRunStep>;
115
+ // The drill-down behind one run: every step it took, and the call chain it belongs to.
116
+ getFlowRunSteps(runId: string): Promise<FlowRunHistory>;
74
117
  completeFlowStep(input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
118
+ // Reading the catalog is the whole of the tool surface here: calling a tool belongs to a flow
119
+ // or to the Intel MCP surface, not to the screen that shows what the portal offers.
75
120
  listTools(): Promise<ToolCatalog>;
76
- testTool(input: TestToolInput): Promise<ToolTestResult>;
77
121
  logout(): Promise<void>;
78
122
  }