@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.
- package/README.md +2 -2
- package/package.json +5 -2
- package/src/agent/agent-avatar/agent-avatar.tsx +34 -0
- package/src/agent/agent-calendar/agent-calendar.tsx +79 -0
- package/src/agent/agent-chat/agent-chat.tsx +116 -0
- package/src/agent/agent-cron/agent-cron.ts +132 -0
- package/src/agent/agent-definition/agent-definition.ts +64 -0
- package/src/agent/agent-entry-title/agent-entry-title.ts +29 -0
- package/src/agent/agent-log/agent-log.tsx +159 -0
- package/src/agent/agent-models/agent-models.ts +63 -0
- package/src/agent/agent-profile/agent-profile.tsx +512 -0
- package/src/agent/agent-state/agent-state.ts +48 -0
- package/src/agent/agent.tsx +361 -0
- package/src/app/app-sidebar/app-sidebar.tsx +2 -2
- package/src/app/app-tree/app-tree.tsx +151 -23
- package/src/app/app.tsx +2 -2
- package/src/app/header-search/header-search.tsx +16 -16
- package/src/app/tree-move/tree-move.tsx +10 -10
- package/src/app/user-footer/user-footer.tsx +7 -1
- package/src/archive/archive.tsx +154 -0
- package/src/components/ui/avatar.tsx +39 -0
- package/src/components/ui/select.tsx +163 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/data/agent-runtime/agent-runtime.ts +93 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +206 -120
- package/src/data/intel-data-provider/intel-data-provider.types.ts +112 -54
- package/src/entry-picker/entry-picker.tsx +53 -17
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +20 -20
- package/src/folder-contents/folder-contents.tsx +7 -7
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/hooks/use-capabilities.ts +22 -0
- package/src/i18n/en.json +156 -64
- package/src/kind-icon.ts +5 -2
- package/src/{knowledge-editor/knowledge-editor.tsx → node-editor/node-editor.tsx} +20 -20
- package/src/{knowledge-graph → node-graph}/graph-notice.tsx +1 -1
- package/src/{knowledge-graph/knowledge-graph.tsx → node-graph/node-graph.tsx} +4 -4
- package/src/{knowledge-table/knowledge-table.tsx → node-table/node-table.tsx} +14 -14
- package/src/{knowledge/knowledge.tsx → nodes/nodes.tsx} +46 -29
- package/src/resource-menu/resource-menu.tsx +105 -62
- package/src/router/router.tsx +17 -5
- package/src/title-row/title-row.tsx +13 -5
- package/vite.config.ts +4 -0
- /package/src/{knowledge-graph/knowledge-graph.ts → node-graph/node-graph.ts} +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
AgentScheduleTarget,
|
|
3
|
+
AppendTableRowsInput,
|
|
4
|
+
AppendTableRowsResult,
|
|
4
5
|
ArchiveFlowInput,
|
|
5
|
-
|
|
6
|
+
ArchiveNodeInput,
|
|
7
|
+
BundleImportResult,
|
|
6
8
|
CompleteFlowRunStepInput,
|
|
9
|
+
CreateAgentInput,
|
|
10
|
+
CreatedAgent,
|
|
7
11
|
CreateFlowInput,
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
CreateNodeInput,
|
|
13
|
+
DefineTableInput,
|
|
10
14
|
Flow,
|
|
11
15
|
FlowDocument,
|
|
12
16
|
FlowList,
|
|
@@ -16,92 +20,114 @@ import type {
|
|
|
16
20
|
FlowRunList,
|
|
17
21
|
FlowRunStep,
|
|
18
22
|
FlowValidation,
|
|
19
|
-
|
|
20
|
-
KnowledgeGraph,
|
|
21
|
-
KnowledgeLinkList,
|
|
22
|
-
KnowledgeNode,
|
|
23
|
-
KnowledgeNodeKind,
|
|
24
|
-
KnowledgeNodeList,
|
|
25
|
-
KnowledgeTable,
|
|
26
|
-
KnowledgeVersionList,
|
|
23
|
+
IntelCapabilities,
|
|
27
24
|
ListFlowRunsInput,
|
|
28
25
|
ListFlowsInput,
|
|
29
|
-
|
|
26
|
+
ListNodesInput,
|
|
27
|
+
Node,
|
|
28
|
+
NodeAgent,
|
|
29
|
+
NodeDocument,
|
|
30
|
+
NodeGraph,
|
|
31
|
+
NodeKind,
|
|
32
|
+
NodeLinkList,
|
|
33
|
+
NodeList,
|
|
34
|
+
NodeTable,
|
|
35
|
+
NodeVersionList,
|
|
30
36
|
PreviewFlowPublishInput,
|
|
31
37
|
PublishFlowInput,
|
|
32
38
|
RelationGraph,
|
|
33
39
|
RelationGraphScope,
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
ResolveNodeLinksInput,
|
|
41
|
+
ResolveNodeLinksResult,
|
|
36
42
|
ResourceGrantList,
|
|
43
|
+
RevokeGrantInput,
|
|
37
44
|
RevokeGrantResult,
|
|
38
|
-
|
|
45
|
+
SaveAgentDefinitionInput,
|
|
46
|
+
SaveAttachmentInput,
|
|
39
47
|
SaveFlowVersionInput,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
SearchKnowledgeResult,
|
|
48
|
+
SaveNodeVersionInput,
|
|
49
|
+
SearchInput,
|
|
50
|
+
SearchResult,
|
|
44
51
|
SessionUser,
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
ShareInput,
|
|
53
|
+
ShareResult,
|
|
47
54
|
StartFlowRunInput,
|
|
48
55
|
ToolCatalog,
|
|
49
56
|
UpdateFlowInput,
|
|
50
|
-
|
|
57
|
+
UpdateNodeInput,
|
|
51
58
|
} from "@anchrd/intel-contract";
|
|
59
|
+
import type {
|
|
60
|
+
AgentManualRun,
|
|
61
|
+
AgentRunDetail,
|
|
62
|
+
AgentRunList,
|
|
63
|
+
AgentState,
|
|
64
|
+
} from "@/data/agent-runtime/agent-runtime.ts";
|
|
52
65
|
|
|
53
|
-
// One row of the shared tree.
|
|
66
|
+
// One row of the shared tree. Nodes and Flows share the folder, not their nature (ADR-0004), so
|
|
54
67
|
// this is a union that keeps each side's record whole — never a merged "node" that is a bit of both.
|
|
55
|
-
// ⚠️ `openable`
|
|
56
|
-
//
|
|
57
|
-
//
|
|
68
|
+
// ⚠️ `openable` means "has something to expand that THIS reader may see" (#59) — not "is of a kind
|
|
69
|
+
// that can be expanded". The difference is the whole point: an arrow on a row with nothing beneath
|
|
70
|
+
// it promises content that expanding does not deliver.
|
|
58
71
|
export type TreeEntry =
|
|
59
72
|
| {
|
|
60
|
-
type: "
|
|
73
|
+
type: "node";
|
|
61
74
|
id: string;
|
|
62
75
|
title: string;
|
|
63
|
-
kind:
|
|
76
|
+
kind: NodeKind;
|
|
64
77
|
openable: boolean;
|
|
65
|
-
node:
|
|
78
|
+
node: Node;
|
|
66
79
|
}
|
|
67
80
|
| { type: "flow"; id: string; title: string; kind: "flow"; openable: boolean; flow: Flow };
|
|
68
81
|
|
|
69
82
|
export interface IntelDataProvider {
|
|
70
83
|
getSession(): Promise<SessionUser>;
|
|
71
|
-
|
|
84
|
+
// What this installation is equipped to do (#190) — deployment facts, not the caller's
|
|
85
|
+
// permissions. `/session` stays identity only on purpose; this is the separate question.
|
|
86
|
+
getCapabilities(): Promise<IntelCapabilities>;
|
|
87
|
+
listNodes(input?: Partial<ListNodesInput>): Promise<NodeList>;
|
|
72
88
|
// One level of the shared tree: the documents and the flows filed in the same folder, in one
|
|
73
89
|
// sorted list. Per level rather than recursive, so opening a folder is what costs a request.
|
|
74
90
|
listTreeChildren(parentId: string | null): Promise<TreeEntry[]>;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
getNode(nodeId: string): Promise<NodeDocument>;
|
|
92
|
+
createNodes(input: CreateNodeInput): Promise<Node>;
|
|
93
|
+
getNodeGraph(limit?: number): Promise<NodeGraph>;
|
|
94
|
+
listNodeLinks(nodeId: string): Promise<NodeLinkList>;
|
|
79
95
|
// The titles of the documents a text links to, for the reader. A target they may not see, or one
|
|
80
96
|
// that is gone, is absent from the answer — the two look the same on purpose (#41).
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
97
|
+
resolveNodeLinks(input: ResolveNodeLinksInput): Promise<ResolveNodeLinksResult>;
|
|
98
|
+
saveNode(input: SaveNodeVersionInput): Promise<NodeDocument>;
|
|
99
|
+
saveNodeAttachment(input: SaveAttachmentInput): Promise<NodeDocument>;
|
|
100
|
+
getNodeAttachment(nodeId: string): Promise<Blob>;
|
|
101
|
+
// One subtree — or one single node — as the zip bundle the server streams (#136). `null` is the
|
|
102
|
+
// root: the whole installation as this caller may read it.
|
|
103
|
+
exportNodeBundle(nodeId: string | null): Promise<Blob>;
|
|
104
|
+
// The same bundle shape for a single flow, through the flow's own export route.
|
|
105
|
+
exportFlowBundle(flowId: string): Promise<Blob>;
|
|
106
|
+
// A zip bundle — or a folder zipped on the client — as a new subtree under the target folder
|
|
107
|
+
// (#137). `nodeId` null files at the root. Always new nodes; the server is all-or-nothing.
|
|
108
|
+
importNodeBundle(input: {
|
|
109
|
+
nodeId: string | null;
|
|
110
|
+
zip: Blob;
|
|
111
|
+
idempotencyKey: string;
|
|
112
|
+
}): Promise<BundleImportResult>;
|
|
113
|
+
// A table as columns and rows. The CSV stays canonical — `getNode` still answers with it,
|
|
86
114
|
// which is what the download uses — and this is the same bytes read by the server's one reader.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
input: AppendKnowledgeTableRowsInput,
|
|
91
|
-
): Promise<AppendKnowledgeTableRowsResult>;
|
|
115
|
+
getNodeTable(nodeId: string): Promise<NodeTable>;
|
|
116
|
+
defineTable(input: DefineTableInput): Promise<NodeTable>;
|
|
117
|
+
appendTableRows(input: AppendTableRowsInput): Promise<AppendTableRowsResult>;
|
|
92
118
|
// Walks the browser through the portal's OAuth flow without asking anybody anything: Gate is the
|
|
93
119
|
// identity provider Cloudflare Access consumes, so a signed-in person is already known there
|
|
94
120
|
// (#60). It navigates away — the caller renders no button for it and gets no answer back.
|
|
95
121
|
startPortalSignIn(returnTo?: string): void;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
listNodeVersions(nodeId: string): Promise<NodeVersionList>;
|
|
123
|
+
updateNode(input: UpdateNodeInput): Promise<Node>;
|
|
124
|
+
archiveNode(input: ArchiveNodeInput): Promise<Node>;
|
|
125
|
+
searchNodes(input: SearchInput): Promise<SearchResult>;
|
|
126
|
+
listGrants(resourceId: string): Promise<ResourceGrantList>;
|
|
101
127
|
// The grant, and what the grant does not cover: the documents the flows in this folder read that
|
|
102
128
|
// the new principal still cannot. A warning, never a refusal (ADR-0004 §4).
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
shareNode(input: ShareInput): Promise<ShareResult>;
|
|
130
|
+
revokeGrant(input: RevokeGrantInput): Promise<RevokeGrantResult>;
|
|
105
131
|
listFlows(input?: ListFlowsInput): Promise<FlowList>;
|
|
106
132
|
getFlow(flowId: string): Promise<FlowDocument>;
|
|
107
133
|
// What a flow calls, read out of its graph. It answers a different question from `listTreeChildren`
|
|
@@ -128,7 +154,7 @@ export interface IntelDataProvider {
|
|
|
128
154
|
publishFlow(input: PublishFlowInput): Promise<Flow>;
|
|
129
155
|
startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
|
|
130
156
|
// What this flow has done, newest first, one page at a time. ⚠️ It carries what a run did and
|
|
131
|
-
// never what it produced: a run reads
|
|
157
|
+
// never what it produced: a run reads nodes with the rights of whoever started it, so its
|
|
132
158
|
// result is not automatically readable for everyone who may read the flow.
|
|
133
159
|
listFlowRuns(input: ListFlowRunsInput): Promise<FlowRunList>;
|
|
134
160
|
getFlowRun(runId: string): Promise<FlowRunStep>;
|
|
@@ -138,5 +164,37 @@ export interface IntelDataProvider {
|
|
|
138
164
|
// Reading the catalog is the whole of the tool surface here: calling a tool belongs to a flow
|
|
139
165
|
// or to the Intel MCP surface, not to the screen that shows what the portal offers.
|
|
140
166
|
listTools(): Promise<ToolCatalog>;
|
|
167
|
+
|
|
168
|
+
// An agent node and the definition its current version carries. `definition` is `null` in the one
|
|
169
|
+
// window where the node exists and no version has been written — the same window a document's
|
|
170
|
+
// content is `null` in.
|
|
171
|
+
getAgent(nodeId: string): Promise<NodeAgent>;
|
|
172
|
+
// ⚠️ `CreatedAgent`: the create answer carries the Gate application key ONCE (#182). Nothing in
|
|
173
|
+
// the UI stores or displays it yet — showing it is its own ticket — but the type has to be honest
|
|
174
|
+
// about what comes back, or the strict read schema would reject the response.
|
|
175
|
+
createAgent(input: CreateAgentInput): Promise<CreatedAgent>;
|
|
176
|
+
// ⚠️ Every change to references, schedules or the model writes a NEW definition version; nothing
|
|
177
|
+
// is edited in place (ADR-0005). `baseVersionId` is the version the editor read, so a parallel
|
|
178
|
+
// change is refused rather than silently overwritten.
|
|
179
|
+
saveAgentDefinition(input: SaveAgentDefinitionInput): Promise<NodeAgent>;
|
|
180
|
+
|
|
181
|
+
// ⚠️ The seven below are answered by the agent RUNTIME, a separate Worker — but they are asked of
|
|
182
|
+
// intel, which proxies them with the caller's own Gate token (#178). One origin, one cookie, no
|
|
183
|
+
// CORS. What that costs is that a 403 here means "you may not drive agents", which is a state the
|
|
184
|
+
// agent page renders as an answer rather than as a failure.
|
|
185
|
+
listAgentRuns(agentId: string): Promise<AgentRunList>;
|
|
186
|
+
getAgentRun(agentId: string, runId: string): Promise<AgentRunDetail>;
|
|
187
|
+
// Whether the agent is switched off. Runtime state, deliberately not part of the definition: a
|
|
188
|
+
// definition is versioned, shared and read into model context (#179).
|
|
189
|
+
getAgentState(agentId: string): Promise<AgentState>;
|
|
190
|
+
pauseAgent(agentId: string): Promise<AgentState>;
|
|
191
|
+
resumeAgent(agentId: string): Promise<AgentState>;
|
|
192
|
+
// Fires one of the agent's own schedules now. The target must be one it already schedules — a run
|
|
193
|
+
// acts with the AGENT's grants, so naming any node here would be borrowing them.
|
|
194
|
+
runAgentNow(agentId: string, target: AgentScheduleTarget): Promise<AgentManualRun>;
|
|
195
|
+
// Where assistant-ui posts. A URL rather than a method: the chat is an SSE stream the transport
|
|
196
|
+
// owns end to end, and handing it a string is the whole of what it needs from this layer.
|
|
197
|
+
agentChatApi(agentId: string): string;
|
|
198
|
+
|
|
141
199
|
logout(): Promise<void>;
|
|
142
200
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NodeKind } from "@anchrd/intel-contract";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { ChevronRight, Folder, Search } from "lucide-react";
|
|
4
4
|
import { useMemo, useState } from "react";
|
|
5
5
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
6
6
|
|
|
7
|
+
// What this picker offers, whichever side of the shared tree it came from. Deliberately the four
|
|
8
|
+
// fields a picker needs and not the whole record: a Node and a Flow are two different things
|
|
9
|
+
// (ADR-0004), and merging them into one row is only allowed where the row is the entire question.
|
|
10
|
+
export type PickerEntry = { id: string; title: string; parentId: string | null; kind: PickerKind };
|
|
11
|
+
export type PickerKind = NodeKind | "flow";
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
|
-
* One picker for everything a node can name: search by name, or click down through the
|
|
9
|
-
* the top-level folders.
|
|
14
|
+
* One picker for everything a node or a flow can name: search by name, or click down through the
|
|
15
|
+
* tree from the top-level folders.
|
|
10
16
|
*
|
|
11
17
|
* ⚠️ Both halves read ONE source — the authorized flat graph the editor already loads. Not because
|
|
12
|
-
* a search endpoint would be hard, but because `
|
|
18
|
+
* a search endpoint would be hard, but because `searchNodes` answers a different question: it
|
|
13
19
|
* searches PASSAGES and hands back citations. Someone looking for a document by name would get
|
|
14
20
|
* results ranked by what is written inside it, which is the wrong list in a picker. Filtering names
|
|
15
21
|
* here is also instant, and it cannot disagree with the tree about what exists.
|
|
@@ -19,29 +25,59 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
|
19
25
|
*/
|
|
20
26
|
export function EntryPicker({
|
|
21
27
|
kind,
|
|
28
|
+
flows = false,
|
|
22
29
|
value,
|
|
23
30
|
onSelect,
|
|
24
31
|
label,
|
|
25
32
|
}: {
|
|
26
33
|
// Which kind of node may be chosen. `null` offers every kind — what the Flow step uses.
|
|
27
|
-
kind:
|
|
34
|
+
kind: NodeKind | null;
|
|
35
|
+
// ⚠️ Flows are a second list, not a kind of node, so they are asked for rather than assumed. An
|
|
36
|
+
// agent's schedule may aim at either (#143); every other caller wants nodes only and pays for no
|
|
37
|
+
// second request.
|
|
38
|
+
flows?: boolean;
|
|
28
39
|
value: string;
|
|
29
|
-
onSelect(
|
|
40
|
+
onSelect(entryId: string, entryKind: PickerKind): void;
|
|
30
41
|
label: string;
|
|
31
42
|
}) {
|
|
32
43
|
const { data, i18n } = useIntelRouterContext();
|
|
33
44
|
const [query, setQuery] = useState("");
|
|
34
45
|
const [openFolder, setOpenFolder] = useState<string | null>(null);
|
|
35
46
|
const graph = useQuery({
|
|
36
|
-
queryKey: ["
|
|
37
|
-
queryFn: () => data.
|
|
47
|
+
queryKey: ["node-graph"],
|
|
48
|
+
queryFn: () => data.getNodeGraph(),
|
|
49
|
+
});
|
|
50
|
+
const flowList = useQuery({
|
|
51
|
+
queryKey: ["flows"],
|
|
52
|
+
queryFn: () => data.listFlows(),
|
|
53
|
+
enabled: flows,
|
|
38
54
|
});
|
|
39
55
|
|
|
40
|
-
const nodes = useMemo(() =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
const nodes = useMemo<PickerEntry[]>(() => {
|
|
57
|
+
const fromNodes = (graph.data?.nodes ?? []).map((node) => ({
|
|
58
|
+
id: node.id,
|
|
59
|
+
title: node.title,
|
|
60
|
+
parentId: node.parentId,
|
|
61
|
+
kind: node.kind as PickerKind,
|
|
62
|
+
}));
|
|
63
|
+
if (!flows) return fromNodes;
|
|
64
|
+
return [
|
|
65
|
+
...fromNodes,
|
|
66
|
+
...(flowList.data?.items ?? []).map((flow) => ({
|
|
67
|
+
id: flow.id,
|
|
68
|
+
title: flow.title,
|
|
69
|
+
parentId: flow.parentId,
|
|
70
|
+
kind: "flow" as const,
|
|
71
|
+
})),
|
|
72
|
+
];
|
|
73
|
+
}, [graph.data, flowList.data, flows]);
|
|
74
|
+
// A flow is offered exactly when it was asked for — it is only in `nodes` at all in that case,
|
|
75
|
+
// and saying so here keeps "may this be picked" one rule rather than two that must agree.
|
|
76
|
+
const pickable = useMemo(
|
|
77
|
+
() => (entry: PickerEntry) => entry.kind === "flow" || kind === null || entry.kind === kind,
|
|
78
|
+
[kind],
|
|
44
79
|
);
|
|
80
|
+
const eligible = useMemo(() => nodes.filter(pickable), [nodes, pickable]);
|
|
45
81
|
|
|
46
82
|
// Searching looks at every eligible node wherever it sits; browsing looks at one level. The two
|
|
47
83
|
// are the same list seen two ways, which is why a result can be picked from either.
|
|
@@ -56,7 +92,7 @@ export function EntryPicker({
|
|
|
56
92
|
|
|
57
93
|
// The way back up, as the chain of folders that leads to the open one.
|
|
58
94
|
const trail = useMemo(() => {
|
|
59
|
-
const chain:
|
|
95
|
+
const chain: PickerEntry[] = [];
|
|
60
96
|
let current = openFolder;
|
|
61
97
|
while (current !== null) {
|
|
62
98
|
const folder = nodes.find((node) => node.id === current);
|
|
@@ -123,20 +159,20 @@ export function EntryPicker({
|
|
|
123
159
|
// is being picked — something to choose. It gets both, side by side, rather than one
|
|
124
160
|
// control that has to guess which was meant.
|
|
125
161
|
const canOpen = node.kind === "folder" && !searching;
|
|
126
|
-
const canPick =
|
|
162
|
+
const canPick = pickable(node);
|
|
127
163
|
return (
|
|
128
|
-
<li key={node.id} className="flex items-center gap-1 px-1">
|
|
164
|
+
<li key={`${node.kind}:${node.id}`} className="flex items-center gap-1 px-1">
|
|
129
165
|
{canPick ? (
|
|
130
166
|
<button
|
|
131
167
|
type="button"
|
|
132
|
-
onClick={() => onSelect(node.id)}
|
|
168
|
+
onClick={() => onSelect(node.id, node.kind)}
|
|
133
169
|
aria-current={node.id === value}
|
|
134
170
|
className={`min-w-0 flex-1 truncate rounded px-2 py-1.5 text-left text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring ${
|
|
135
171
|
node.id === value ? "bg-muted font-medium" : ""
|
|
136
172
|
}`}
|
|
137
173
|
>
|
|
138
174
|
{node.title}
|
|
139
|
-
<span className="sr-only"> — {i18n.t(`
|
|
175
|
+
<span className="sr-only"> — {i18n.t(`node.kind.${node.kind}`)}</span>
|
|
140
176
|
</button>
|
|
141
177
|
) : (
|
|
142
178
|
<span className="min-w-0 flex-1 truncate px-2 py-1.5 text-sm text-muted-foreground">
|
|
@@ -24,7 +24,7 @@ function formatDuration(ms: number, i18n: I18n): string {
|
|
|
24
24
|
* already built rather than opening somewhere new.
|
|
25
25
|
*
|
|
26
26
|
* ⚠️ What it shows is what a run *did*. Neither the input a run was given nor the result it
|
|
27
|
-
* produced is in the answer it reads, because a run reaches
|
|
27
|
+
* produced is in the answer it reads, because a run reaches nodes and tools with the rights of
|
|
28
28
|
* whoever started it — and the person reading this list is not necessarily that person.
|
|
29
29
|
*/
|
|
30
30
|
export function FlowRuns({ flowId }: { flowId: string }) {
|
package/src/flows/flows.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Flow, FlowGraph, FlowNode,
|
|
1
|
+
import type { Flow, FlowGraph, FlowNode, Node } from "@anchrd/intel-contract";
|
|
2
2
|
import { flowNodeLayer } from "@anchrd/intel-contract";
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
type EdgeChange,
|
|
14
14
|
Handle,
|
|
15
15
|
MiniMap,
|
|
16
|
-
type Node,
|
|
17
16
|
type NodeChange,
|
|
18
17
|
type NodeProps,
|
|
19
18
|
Position,
|
|
20
19
|
ReactFlow,
|
|
20
|
+
type Node as ReactFlowNode,
|
|
21
21
|
ReactFlowProvider,
|
|
22
22
|
useReactFlow,
|
|
23
23
|
} from "@xyflow/react";
|
|
@@ -39,7 +39,7 @@ import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
|
|
|
39
39
|
import { useSystemTheme } from "@/theme/theme.ts";
|
|
40
40
|
import { TitleRow } from "@/title-row/title-row.tsx";
|
|
41
41
|
|
|
42
|
-
type CanvasNode =
|
|
42
|
+
type CanvasNode = ReactFlowNode<{ node: FlowNode }, "intel">;
|
|
43
43
|
type CanvasEdge = Edge;
|
|
44
44
|
|
|
45
45
|
// One handle id at both ends of a context edge. React Flow reports the handles a connection was
|
|
@@ -269,7 +269,7 @@ const nodeKindOfLink = Object.fromEntries(
|
|
|
269
269
|
Object.entries(flowKindOfEntry)
|
|
270
270
|
.filter(([, kind]) => kind !== undefined && kind !== "subflow")
|
|
271
271
|
.map(([nodeKind, kind]) => [kind, nodeKind]),
|
|
272
|
-
) as Partial<Record<FlowNode["kind"],
|
|
272
|
+
) as Partial<Record<FlowNode["kind"], Node["kind"]>>;
|
|
273
273
|
|
|
274
274
|
type LinkNode = Extract<FlowNode, { configuration: { resourceId: string } }>;
|
|
275
275
|
|
|
@@ -282,7 +282,7 @@ function isLinkNode(node: FlowNode): node is LinkNode {
|
|
|
282
282
|
function newNode(
|
|
283
283
|
kind: FlowNode["kind"],
|
|
284
284
|
index: number,
|
|
285
|
-
|
|
285
|
+
firstNodeId?: string,
|
|
286
286
|
firstFlowId?: string,
|
|
287
287
|
): FlowNode {
|
|
288
288
|
const common = {
|
|
@@ -309,7 +309,7 @@ function newNode(
|
|
|
309
309
|
case "document":
|
|
310
310
|
case "upload":
|
|
311
311
|
case "table":
|
|
312
|
-
return { ...common, kind, configuration: { resourceId:
|
|
312
|
+
return { ...common, kind, configuration: { resourceId: firstNodeId ?? "" } };
|
|
313
313
|
case "tool":
|
|
314
314
|
return {
|
|
315
315
|
...common,
|
|
@@ -363,11 +363,11 @@ function FlowsEditor() {
|
|
|
363
363
|
queryKey: ["tools"],
|
|
364
364
|
queryFn: () => data.listTools(),
|
|
365
365
|
});
|
|
366
|
-
// A
|
|
366
|
+
// A tree link points at a node, so the editor needs candidates. The graph is the authorized
|
|
367
367
|
// flat list of them; walking the folder tree for a picker would be the N+1 all over again.
|
|
368
|
-
const
|
|
369
|
-
queryKey: ["
|
|
370
|
-
queryFn: () => data.
|
|
368
|
+
const nodeGraph = useQuery({
|
|
369
|
+
queryKey: ["node-graph"],
|
|
370
|
+
queryFn: () => data.getNodeGraph(),
|
|
371
371
|
});
|
|
372
372
|
// The candidates for a subflow step. Which of them a flow may actually call is answered when it is
|
|
373
373
|
// published (ADR-0004 §3) — the picker offers what the user may see, and the refusal names the
|
|
@@ -396,7 +396,7 @@ function FlowsEditor() {
|
|
|
396
396
|
// Lifted out of the bar because the canvas dismisses it too — a click on the pane is the third way
|
|
397
397
|
// out, next to the trigger and Escape.
|
|
398
398
|
const [paletteOpen, setPaletteOpen] = usePaletteOpen();
|
|
399
|
-
// A flow is shared through the folder it is filed in, on the
|
|
399
|
+
// A flow is shared through the folder it is filed in, on the Intelligence screen (ADR-0004 §2). It
|
|
400
400
|
// has no share action of its own — a narrower grant beside the folder's would break the guarantee
|
|
401
401
|
// that a flow only reaches flows in its own subtree.
|
|
402
402
|
const document = useQuery({
|
|
@@ -483,7 +483,7 @@ function FlowsEditor() {
|
|
|
483
483
|
query={relations}
|
|
484
484
|
select={(node) =>
|
|
485
485
|
void navigate({
|
|
486
|
-
to: node.kind === "flow" ? "/flows" : "/
|
|
486
|
+
to: node.kind === "flow" ? "/flows" : "/nodes",
|
|
487
487
|
search: { select: node.id },
|
|
488
488
|
})
|
|
489
489
|
}
|
|
@@ -525,7 +525,7 @@ function FlowsEditor() {
|
|
|
525
525
|
const item = newNode(
|
|
526
526
|
kind,
|
|
527
527
|
nodes.length,
|
|
528
|
-
|
|
528
|
+
nodeGraph.data?.nodes[0]?.id,
|
|
529
529
|
callable.data?.items.find((entry) => entry.id !== selectedFlowId)?.id,
|
|
530
530
|
);
|
|
531
531
|
setNodes((current) => [
|
|
@@ -1010,8 +1010,8 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1010
1010
|
// answers, so the counted ones keep the panel from claiming the first when it means the second.
|
|
1011
1011
|
const empty =
|
|
1012
1012
|
needs.data !== undefined &&
|
|
1013
|
-
needs.data.
|
|
1014
|
-
needs.data.
|
|
1013
|
+
needs.data.nodes.length === 0 &&
|
|
1014
|
+
needs.data.hiddenNodes === 0 &&
|
|
1015
1015
|
needs.data.tools.length === 0;
|
|
1016
1016
|
return (
|
|
1017
1017
|
<section aria-label={i18n.t("flows.needs")} className="mt-auto border-t p-5">
|
|
@@ -1027,13 +1027,13 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1027
1027
|
{empty && <p className="mt-2 text-sm text-muted-foreground">{i18n.t("flows.needsEmpty")}</p>}
|
|
1028
1028
|
{needs.data && !empty && (
|
|
1029
1029
|
<>
|
|
1030
|
-
{(needs.data.
|
|
1030
|
+
{(needs.data.nodes.length > 0 || needs.data.hiddenNodes > 0) && (
|
|
1031
1031
|
<>
|
|
1032
1032
|
<h3 className="mt-4 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
1033
|
-
{i18n.t("flows.
|
|
1033
|
+
{i18n.t("flows.needsNodes")}
|
|
1034
1034
|
</h3>
|
|
1035
1035
|
<ul className="mt-2 space-y-1 text-sm">
|
|
1036
|
-
{needs.data.
|
|
1036
|
+
{needs.data.nodes.map((reference) => (
|
|
1037
1037
|
<li key={reference.id} className="flex items-center gap-2">
|
|
1038
1038
|
<FileSearch aria-hidden="true" className="size-3.5 shrink-0" />
|
|
1039
1039
|
<span className="min-w-0 truncate">{reference.title}</span>
|
|
@@ -1041,9 +1041,9 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1041
1041
|
))}
|
|
1042
1042
|
{/* ⚠️ Counted, never named. A title is exactly what someone without access to the
|
|
1043
1043
|
document may not learn from a list about it. */}
|
|
1044
|
-
{needs.data.
|
|
1044
|
+
{needs.data.hiddenNodes > 0 && (
|
|
1045
1045
|
<li className="text-muted-foreground">
|
|
1046
|
-
{i18n.t("flows.needsHidden", { count: needs.data.
|
|
1046
|
+
{i18n.t("flows.needsHidden", { count: needs.data.hiddenNodes })}
|
|
1047
1047
|
</li>
|
|
1048
1048
|
)}
|
|
1049
1049
|
</ul>
|
|
@@ -19,7 +19,7 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
|
19
19
|
function targetOf(entry: TreeEntry): ResourceTarget {
|
|
20
20
|
return entry.type === "flow"
|
|
21
21
|
? { type: "flow", flow: entry.flow }
|
|
22
|
-
: { type: "
|
|
22
|
+
: { type: "node", node: entry.node };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -51,7 +51,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
51
51
|
if (level.isError) {
|
|
52
52
|
return (
|
|
53
53
|
<p role="alert" className="p-6 text-sm text-destructive">
|
|
54
|
-
{i18n.t("
|
|
54
|
+
{i18n.t("node.folderFailed")}
|
|
55
55
|
</p>
|
|
56
56
|
);
|
|
57
57
|
}
|
|
@@ -63,7 +63,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
63
63
|
if (entries.length === 0) {
|
|
64
64
|
return (
|
|
65
65
|
<div className="grid flex-1 place-items-center p-8 text-sm text-muted-foreground">
|
|
66
|
-
{i18n.t("
|
|
66
|
+
{i18n.t("node.folderEmpty")}
|
|
67
67
|
</div>
|
|
68
68
|
);
|
|
69
69
|
}
|
|
@@ -78,12 +78,12 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
78
78
|
{i18n.t("common.title")}
|
|
79
79
|
</TableHead>
|
|
80
80
|
<TableHead className="h-9 px-4 text-right text-xs tracking-wide uppercase">
|
|
81
|
-
{i18n.t("
|
|
81
|
+
{i18n.t("node.changed")}
|
|
82
82
|
</TableHead>
|
|
83
83
|
{/* A `<th>` with nothing in it leaves the column unnamed for anyone reading the table
|
|
84
84
|
by its headers. The word is there; only the eye is spared it. */}
|
|
85
85
|
<TableHead className="h-9 w-14">
|
|
86
|
-
<span className="sr-only">{i18n.t("
|
|
86
|
+
<span className="sr-only">{i18n.t("node.rowActions")}</span>
|
|
87
87
|
</TableHead>
|
|
88
88
|
</TableRow>
|
|
89
89
|
</TableHeader>
|
|
@@ -101,7 +101,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
101
101
|
type="button"
|
|
102
102
|
onClick={() =>
|
|
103
103
|
void navigate({
|
|
104
|
-
to: entry.type === "flow" ? "/flows" : "/
|
|
104
|
+
to: entry.type === "flow" ? "/flows" : "/nodes",
|
|
105
105
|
search: { select: entry.id },
|
|
106
106
|
})
|
|
107
107
|
}
|
|
@@ -120,7 +120,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
120
120
|
</span>
|
|
121
121
|
{/* The icon carries the type on screen; this carries it everywhere else, and
|
|
122
122
|
it is why a flow needs no suffix in its name to be told from a document. */}
|
|
123
|
-
<span className="sr-only">{i18n.t(`
|
|
123
|
+
<span className="sr-only">{i18n.t(`node.kind.${entry.kind}`)}</span>
|
|
124
124
|
</button>
|
|
125
125
|
</TableCell>
|
|
126
126
|
<TableCell className="px-4 text-right text-sm text-muted-foreground tabular-nums">
|
|
@@ -4,7 +4,7 @@ import { lazy, Suspense } from "react";
|
|
|
4
4
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
5
5
|
|
|
6
6
|
const RelationGraphView = lazy(async () => ({
|
|
7
|
-
default: (await import("@/
|
|
7
|
+
default: (await import("@/node-graph/node-graph.tsx")).RelationGraphView,
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
10
|
// The graph half of the editor ↔ graph switch, with its own loading, error and empty states (#19).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IntelCapabilities } from "@anchrd/intel-contract";
|
|
2
|
+
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What this installation is equipped to do, asked once and believed for the session (#190).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ Deployment facts, not the caller's permissions — a runtime does not appear or vanish while a
|
|
9
|
+
* page is open, so the answer never goes stale and is never refetched. While the answer is still on
|
|
10
|
+
* its way, `data` is `undefined`: a caller deciding whether to OFFER something treats that as "not
|
|
11
|
+
* yet" (a menu item appearing late is fine; one that vanishes was a lie), and a caller deciding
|
|
12
|
+
* whether to take a rendered page AWAY does the opposite.
|
|
13
|
+
*/
|
|
14
|
+
export function useCapabilities(): UseQueryResult<IntelCapabilities> {
|
|
15
|
+
const { data } = useIntelRouterContext();
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: ["capabilities"],
|
|
18
|
+
queryFn: () => data.getCapabilities(),
|
|
19
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
20
|
+
retry: false,
|
|
21
|
+
});
|
|
22
|
+
}
|