@anchrd/intel-ui 0.4.0 → 0.6.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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/app/action-slot/action-slot.tsx +27 -0
  3. package/src/app/app-sidebar/app-sidebar.tsx +39 -24
  4. package/src/app/app-tree/app-tree.tsx +332 -60
  5. package/src/app/app.tsx +31 -5
  6. package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +2 -1
  7. package/src/app/tree-move/tree-move.tsx +331 -0
  8. package/src/app/user-footer/user-footer.tsx +73 -46
  9. package/src/app/view-toggle/view-toggle.tsx +77 -0
  10. package/src/blocknote-view/blocknote-view.tsx +19 -2
  11. package/src/branding/favicon.default.svg +2 -2
  12. package/src/branding/favicon.svg +2 -2
  13. package/src/components/ui/dropdown-menu.tsx +78 -0
  14. package/src/data/intel-data-provider/intel-data-provider.ts +135 -57
  15. package/src/data/intel-data-provider/intel-data-provider.types.ts +51 -13
  16. package/src/document-link/document-link.tsx +132 -0
  17. package/src/flow-runs/flow-runs.tsx +225 -0
  18. package/src/flows/flows.tsx +665 -271
  19. package/src/flows/node-icon/node-icon.ts +28 -0
  20. package/src/flows/node-palette/node-palette.tsx +200 -0
  21. package/src/flows/node-palette/node-palette.types.ts +15 -0
  22. package/src/graph-pane/graph-pane.tsx +44 -0
  23. package/src/i18n/en.json +144 -29
  24. package/src/knowledge/knowledge.tsx +91 -367
  25. package/src/knowledge-editor/knowledge-editor.tsx +169 -21
  26. package/src/knowledge-graph/knowledge-graph.ts +26 -24
  27. package/src/knowledge-graph/knowledge-graph.tsx +33 -24
  28. package/src/knowledge-table/knowledge-table.tsx +141 -0
  29. package/src/main.tsx +2 -2
  30. package/src/resource-menu/resource-menu.tsx +615 -0
  31. package/src/router/selection-search.ts +27 -3
  32. package/src/save-button/save-button.tsx +103 -0
  33. package/src/styles.css +37 -0
  34. package/src/theme/theme.ts +24 -0
  35. package/src/title-row/title-row.tsx +49 -0
  36. package/src/tools/tools.tsx +57 -38
  37. package/src/app/header-actions/header-actions.tsx +0 -15
@@ -0,0 +1,28 @@
1
+ import type { FlowNode } from "@anchrd/intel-contract";
2
+ import {
3
+ CheckCircle2,
4
+ CirclePlay,
5
+ FileSearch,
6
+ GitBranch,
7
+ ShieldCheck,
8
+ Sparkles,
9
+ Workflow,
10
+ Wrench,
11
+ } from "lucide-react";
12
+ import type { ComponentType, SVGProps } from "react";
13
+
14
+ // The canvas card and the palette entry have to show the same symbol for the same kind — a step that
15
+ // looks one way in the bar and another on the graph is two things to learn instead of one. The map
16
+ // lives apart from both so neither has to import the other.
17
+ export type NodeIcon = ComponentType<SVGProps<SVGSVGElement>>;
18
+
19
+ export const nodeIcon: Record<FlowNode["kind"], NodeIcon> = {
20
+ trigger: CirclePlay,
21
+ instruction: Sparkles,
22
+ knowledge: FileSearch,
23
+ tool: Wrench,
24
+ condition: GitBranch,
25
+ approval: ShieldCheck,
26
+ subflow: Workflow,
27
+ output: CheckCircle2,
28
+ };
@@ -0,0 +1,200 @@
1
+ import { Plus, X } from "lucide-react";
2
+ import { useId, useRef, useState } from "react";
3
+ import { type NodeIcon, nodeIcon } from "@/flows/node-icon/node-icon.ts";
4
+ import { cn } from "@/lib/utils.ts";
5
+ import { useIntelRouterContext } from "@/router/router-context.ts";
6
+ import type { NodePaletteProps, PaletteKind } from "./node-palette.types.ts";
7
+
8
+ const storageKey = "intel.flow-palette";
9
+
10
+ // ⚠️ Reading `localStorage` throws outright in a few privacy modes, so the guard is a try, not a
11
+ // typeof check — the same reason `sidebar-preferences` guards it that way. Losing the preference is
12
+ // acceptable; losing the editor is not.
13
+ function readOpen(): boolean {
14
+ try {
15
+ return typeof window !== "undefined" && window.localStorage.getItem(storageKey) === "open";
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function writeOpen(open: boolean): void {
22
+ try {
23
+ if (typeof window !== "undefined")
24
+ window.localStorage.setItem(storageKey, open ? "open" : "closed");
25
+ } catch {}
26
+ }
27
+
28
+ // Closed is the default, and the preference is written through: the bar is chrome, exactly like the
29
+ // sidebar whose open state already survives a reload. Someone who builds flows all day would
30
+ // otherwise re-open it on every load, and the ticket's own reason for collapsing it — the canvas
31
+ // belongs to the graph — is just as true for someone who never opens it.
32
+ export function usePaletteOpen(): [boolean, (open: boolean) => void] {
33
+ const [open, setOpen] = useState(readOpen);
34
+ return [
35
+ open,
36
+ (next: boolean) => {
37
+ setOpen(next);
38
+ writeOpen(next);
39
+ },
40
+ ];
41
+ }
42
+
43
+ export function NodePalette<K extends PaletteKind>({
44
+ kinds,
45
+ open,
46
+ setOpen,
47
+ add,
48
+ disabledReason,
49
+ }: NodePaletteProps<K>) {
50
+ const { i18n } = useIntelRouterContext();
51
+ const listId = useId();
52
+ const triggerRef = useRef<HTMLButtonElement>(null);
53
+ const listRef = useRef<HTMLDivElement>(null);
54
+ // A toolbar carries one tab stop, so the group is entered with a single Tab and left with the
55
+ // next one instead of costing seven.
56
+ const [active, setActive] = useState(0);
57
+
58
+ // The focus goes back where it came from, so closing does not drop the caret at the top of the
59
+ // document and make the next Tab start over from the page.
60
+ function close() {
61
+ setOpen(false);
62
+ triggerRef.current?.focus();
63
+ }
64
+
65
+ function entries(): HTMLButtonElement[] {
66
+ return [
67
+ ...(listRef.current?.querySelectorAll<HTMLButtonElement>("button[data-palette-item]") ?? []),
68
+ ];
69
+ }
70
+
71
+ // A disabled entry is stepped over rather than focused: its reason is already on the entry, and
72
+ // stopping there would only be a dead end for the keyboard.
73
+ function focusEntry(from: number, delta: number) {
74
+ const items = entries();
75
+ if (items.length === 0) return;
76
+ for (let step = 1; step <= items.length; step += 1) {
77
+ const index = (((from + delta * step) % items.length) + items.length) % items.length;
78
+ const item = items[index];
79
+ if (item && !item.disabled) {
80
+ setActive(index);
81
+ item.focus();
82
+ return;
83
+ }
84
+ }
85
+ }
86
+
87
+ function onListKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
88
+ if (event.key === "Escape") {
89
+ event.stopPropagation();
90
+ close();
91
+ return;
92
+ }
93
+ const forward = event.key === "ArrowDown" || event.key === "ArrowRight";
94
+ const backward = event.key === "ArrowUp" || event.key === "ArrowLeft";
95
+ if (!forward && !backward) return;
96
+ event.preventDefault();
97
+ const items = entries();
98
+ const current = items.indexOf(document.activeElement as HTMLButtonElement);
99
+ focusEntry(current < 0 ? 0 : current, forward ? 1 : -1);
100
+ }
101
+
102
+ function onTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
103
+ if (!open) return;
104
+ if (event.key === "Escape") {
105
+ event.stopPropagation();
106
+ close();
107
+ return;
108
+ }
109
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
110
+ event.preventDefault();
111
+ focusEntry(-1, 1);
112
+ }
113
+ }
114
+
115
+ // Open, the trigger is the first segment of the bar itself rather than a second box above it: one
116
+ // closes a bar where it is. Two details carry that and neither is decoration:
117
+ //
118
+ // ⚠️ The negative margin is the frame's own padding plus its border. Opening hands the frame from
119
+ // the trigger to the row around it, and a frame drawn inside would push the trigger down and right
120
+ // by exactly that sum. The margin lets the row grow outwards instead, so the 36px one clicks stays
121
+ // on the same pixel open and collapsed. Change `p-1` and this has to change with it.
122
+ //
123
+ // ⚠️ The toolbar stays an element of its own inside the row: it is what `aria-controls` names and
124
+ // what the arrow keys walk, and the trigger must not become an eighth entry in that ring.
125
+ //
126
+ // The `w-max` that used to stand here went with the structure it guarded. While the bar was a
127
+ // block below the trigger, the shrink-wrapped box took the collapsed 36px as its maximum and
128
+ // folded the entries into a column; one flex row is sized from its content in either state, which
129
+ // was measured at 1280px and at 420px with the class and without it (#55).
130
+ return (
131
+ <div
132
+ className={cn(
133
+ "absolute left-4 top-4 z-10 max-w-[calc(100%-2rem)]",
134
+ open &&
135
+ "-m-[calc(0.25rem+1px)] flex flex-wrap items-center gap-1 rounded-lg border bg-card/95 p-1 shadow-sm backdrop-blur",
136
+ )}
137
+ >
138
+ <button
139
+ ref={triggerRef}
140
+ type="button"
141
+ aria-expanded={open}
142
+ aria-controls={listId}
143
+ onClick={() => (open ? close() : setOpen(true))}
144
+ onKeyDown={onTriggerKeyDown}
145
+ className={cn(
146
+ "inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-card-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring",
147
+ !open && "border bg-card/95 shadow-sm backdrop-blur",
148
+ )}
149
+ >
150
+ {open ? (
151
+ <X aria-hidden="true" className="size-4" />
152
+ ) : (
153
+ <Plus aria-hidden="true" className="size-4" />
154
+ )}
155
+ <span className="sr-only">{i18n.t(open ? "flows.closePalette" : "flows.addNode")}</span>
156
+ </button>
157
+ {/* Over the canvas rather than pushing it: the graph is a spatial workspace, and resizing the
158
+ viewport would shift every node under the pointer each time the bar opens. It is dismissed
159
+ instead — the trigger, Escape, or a click on the canvas. */}
160
+ {open && (
161
+ <div
162
+ ref={listRef}
163
+ id={listId}
164
+ role="toolbar"
165
+ aria-orientation="horizontal"
166
+ aria-label={i18n.t("flows.nodePalette")}
167
+ onKeyDown={onListKeyDown}
168
+ // `min-w-0` so a narrow bar makes the entries wrap among themselves instead of forcing the
169
+ // whole row wider than the canvas allows.
170
+ className="flex min-w-0 max-w-full flex-wrap gap-1"
171
+ >
172
+ {kinds.map((kind, index) => {
173
+ const Icon: NodeIcon = nodeIcon[kind];
174
+ const reason = disabledReason?.(kind) ?? null;
175
+ return (
176
+ <button
177
+ key={kind}
178
+ type="button"
179
+ data-palette-item=""
180
+ disabled={reason !== null}
181
+ title={reason ?? undefined}
182
+ tabIndex={index === active ? 0 : -1}
183
+ onClick={() => {
184
+ setActive(index);
185
+ add(kind);
186
+ }}
187
+ // `h-9` is the trigger's height: sharing one row only reads as one row if the entries
188
+ // start on the trigger's top edge instead of floating in the middle of it.
189
+ className="inline-flex h-9 items-center gap-1.5 rounded-md px-2 text-xs outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
190
+ >
191
+ <Icon aria-hidden="true" className="size-3.5" />
192
+ {i18n.t(`flows.node.${kind}`)}
193
+ </button>
194
+ );
195
+ })}
196
+ </div>
197
+ )}
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,15 @@
1
+ import type { FlowNode } from "@anchrd/intel-contract";
2
+
3
+ export type PaletteKind = FlowNode["kind"];
4
+
5
+ // Generic over the kinds it is given: the caller decides which subset the bar offers, and `add` is
6
+ // handed back exactly that subset rather than "any node kind" it would have to re-narrow.
7
+ export interface NodePaletteProps<K extends PaletteKind = PaletteKind> {
8
+ kinds: readonly K[];
9
+ open: boolean;
10
+ setOpen(open: boolean): void;
11
+ add(kind: K): void;
12
+ // Why an entry is off, in words, or `null` when it is not. One function rather than a flag and a
13
+ // string: an entry that is greyed out without a reason is just a broken button.
14
+ disabledReason?(kind: K): string | null;
15
+ }
@@ -0,0 +1,44 @@
1
+ import type { RelationGraph } from "@anchrd/intel-contract";
2
+ import type { UseQueryResult } from "@tanstack/react-query";
3
+ import { lazy, Suspense } from "react";
4
+ import { useIntelRouterContext } from "@/router/router-context.ts";
5
+
6
+ const RelationGraphView = lazy(async () => ({
7
+ default: (await import("@/knowledge-graph/knowledge-graph.tsx")).RelationGraphView,
8
+ }));
9
+
10
+ // The graph half of the editor ↔ graph switch, with its own loading, error and empty states (#19).
11
+ // One component for both screens: a folder and a single flow ask the same question of the same
12
+ // endpoint, and two copies of this would drift the moment one of them grew a state.
13
+ export function GraphPane({
14
+ query,
15
+ select,
16
+ }: {
17
+ query: UseQueryResult<RelationGraph>;
18
+ select(node: RelationGraph["nodes"][number]): void;
19
+ }) {
20
+ const { i18n } = useIntelRouterContext();
21
+ const loading = <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>;
22
+ if (query.isPending) return loading;
23
+ if (query.isError || !query.data) {
24
+ return (
25
+ <div role="alert" className="grid flex-1 place-items-center p-8 text-center text-sm">
26
+ <div className="space-y-3">
27
+ <p className="text-destructive">{i18n.t("view.graphFailed")}</p>
28
+ <button
29
+ type="button"
30
+ onClick={() => void query.refetch()}
31
+ className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
32
+ >
33
+ {i18n.t("common.retry")}
34
+ </button>
35
+ </div>
36
+ </div>
37
+ );
38
+ }
39
+ return (
40
+ <Suspense fallback={loading}>
41
+ <RelationGraphView data={query.data} i18n={i18n} select={select} />
42
+ </Suspense>
43
+ );
44
+ }
package/src/i18n/en.json CHANGED
@@ -2,13 +2,14 @@
2
2
  "$locale": "en-US",
3
3
  "app.name": "Intel",
4
4
  "nav.primary": "Primary navigation",
5
- "nav.knowledge": "Knowledge",
5
+ "nav.knowledge": "Intelligence",
6
6
  "nav.flows": "Flows",
7
7
  "nav.tools": "Tools",
8
8
  "shell.toggleSidebar": "Collapse or expand the sidebar",
9
9
  "shell.resizeSidebar": "Sidebar width",
10
10
  "shell.signedInUser": "Signed-in user",
11
11
  "shell.userUnavailable": "Account unavailable",
12
+ "shell.userMenu": "Account and tools for {name}",
12
13
  "search.open": "Search",
13
14
  "search.title": "Search Intel",
14
15
  "search.description": "Find knowledge, flows, and tools you are allowed to see.",
@@ -23,27 +24,65 @@
23
24
  "search.empty": "Nothing found for “{query}”.",
24
25
  "search.failed": "The search could not be completed.",
25
26
  "tree.label": "Knowledge and flows",
27
+ "tree.contents": "Inside {title}",
28
+ "tree.calls": "Flows called by {title}",
26
29
  "tree.empty": "Nothing here yet.",
27
- "tree.emptyFolder": "This folder is empty.",
28
30
  "tree.failed": "This part of the tree could not be loaded.",
29
31
  "tree.expand": "Show what is inside {title}",
30
32
  "tree.collapse": "Hide what is inside {title}",
33
+ "tree.expandCalls": "Show the flows {title} calls",
34
+ "tree.collapseCalls": "Hide the flows {title} calls",
35
+ "tree.noCalls": "This flow calls no other flow.",
31
36
  "tree.add": "Add to {title}",
32
37
  "tree.addRoot": "Add at the top level",
33
38
  "tree.new.folder": "New folder",
34
39
  "tree.new.document": "New document",
40
+ "tree.new.table": "New table",
35
41
  "tree.new.upload": "Upload file",
36
42
  "tree.new.flow": "New flow",
37
43
  "tree.upload": "File to upload",
44
+ "tree.table.columns": "Columns",
45
+ "tree.table.columnsHint": "Separate the column names with commas. They are the header, and every row appended later must match it.",
38
46
  "tree.createFailed": "It could not be created. Check your access and try again.",
39
47
  "tree.uploadFailed": "The file could not be uploaded. Check the 15 MB limit and try again.",
40
48
  "tree.kind.folder": "Folder",
41
49
  "tree.kind.document": "Document",
42
50
  "tree.kind.attachment": "File",
51
+ "tree.kind.table": "Table",
43
52
  "tree.kind.flow": "Flow",
53
+ "tree.move.action": "Move to…",
54
+ "tree.move.title": "Move {title}",
55
+ "tree.move.root": "Top level",
56
+ "tree.move.dropRoot": "Drop here to move to the top level",
57
+ "tree.move.pick": "Choose the folder to move it into.",
58
+ "tree.move.up": "Back to the folder above",
59
+ "tree.move.open": "Open {title}",
60
+ "tree.move.noFolders": "There is no folder here to move it into.",
61
+ "tree.move.here": "Move into {title}",
62
+ "tree.move.alreadyThere": "It is already filed here.",
63
+ "tree.move.destination": "New place: {title}",
64
+ "tree.move.sharingWarning": "Sharing follows the folder. After the move, everyone who can reach {title} can reach this entry, and anyone who only reached it through its old folder loses it. Nobody is notified.",
65
+ "tree.move.elsewhere": "Choose another folder",
66
+ "tree.move.confirm": "Move",
67
+ "tree.move.failed.forbidden": "It was not moved: you may not write into that folder. Ask for write access there.",
68
+ "tree.move.failed.notFolder": "It was not moved: only a folder can hold other entries.",
69
+ "tree.move.failed.cycle": "It was not moved: a folder cannot be put inside itself or inside anything it contains.",
70
+ "tree.move.failed.conflict": "It was not moved: somebody else changed this entry first. The tree has been reloaded — look again, then move it.",
71
+ "tree.move.failed.gone": "It was not moved: that folder no longer exists.",
72
+ "tree.move.failed.other": "It could not be moved. Check your access and try again.",
73
+ "resource.menu": "More actions for {title}",
74
+ "resource.rename": "Rename",
75
+ "resource.renameTitle": "Rename {title}",
76
+ "resource.conflict": "It was not changed: somebody else changed this entry first. Reload it and try again.",
77
+ "resource.forbidden": "It was not changed: you may not change this entry.",
78
+ "resource.failed": "The change was not saved. Reload the latest version and try again.",
44
79
  "knowledge.empty": "No knowledge has been added yet.",
45
80
  "knowledge.download": "Download file",
46
81
  "knowledge.attachmentHelp": "The canonical file is stored privately in Intel. Its AI-readable projection is indexed separately.",
82
+ "knowledge.table.summary": "{rows} rows, {columns} columns",
83
+ "knowledge.table.download": "Download CSV",
84
+ "knowledge.table.empty": "No rows yet. Rows are appended by flows and agents.",
85
+ "knowledge.table.undefined": "This table has no columns yet.",
47
86
  "knowledge.select": "Select a document or folder to work with it.",
48
87
  "knowledge.folderHelp": "Select a child document, or create something inside this folder.",
49
88
  "knowledge.contextPolicy": "AI context policy",
@@ -53,39 +92,55 @@
53
92
  "knowledge.share": "Share",
54
93
  "knowledge.shareAction": "Grant access",
55
94
  "knowledge.email": "Email address",
56
- "knowledge.role": "Access level",
57
- "knowledge.viewer": "Can view",
58
- "knowledge.editor": "Can edit",
59
- "knowledge.manager": "Can manage and share",
95
+ "knowledge.verbs": "What this grant allows",
96
+ "knowledge.verb.read": "Read",
97
+ "knowledge.verb.write": "Write",
98
+ "knowledge.verb.execute": "Run",
99
+ "knowledge.verb.share": "Share",
100
+ "knowledge.verbHint.read": "See everything in here",
101
+ "knowledge.verbHint.write": "Change and add things",
102
+ "knowledge.verbHint.execute": "Start the flows in here",
103
+ "knowledge.verbHint.share": "Give others access",
104
+ "knowledge.organization": "Everyone in the organization",
105
+ "knowledge.revokeShare": "Revoke access",
106
+ "knowledge.noGrants": "Nobody outside the owner has access yet.",
107
+ "knowledge.shareFailed": "Access was not changed. Check your permission and try again.",
108
+ "knowledge.shareUnreadable": "Flows in this folder read documents this grant does not cover: {titles}.",
109
+ "knowledge.shareUnreadableMore": "{count} more are out of reach too, and you cannot see them.",
110
+ "knowledge.shareUnreadableHidden": "{count} documents that flows in this folder read are out of reach with this grant. You cannot see them.",
111
+ "knowledge.shareUnreadableHint": "Nothing is blocked. A run of those flows will simply stop at that step for them.",
60
112
  "knowledge.versions": "Version history",
61
113
  "knowledge.version": "Version {sequence}",
62
114
  "knowledge.archive": "Archive",
63
- "knowledge.graph": "Knowledge graph",
64
- "knowledge.graphDescription": "{nodes} authorized nodes and {links} explicit relationships.",
115
+ "view.showGraph": "Show this level as a graph",
116
+ "view.showEditor": "Back to the editor",
117
+ "view.showRuns": "Show this flow's runs",
118
+ "view.graph": "Relation graph",
119
+ "view.graphDescription": "{nodes} nodes and {edges} relationships you are allowed to see.",
120
+ "view.graphTruncated": "{omitted} more left out — this view draws at most {limit}.",
121
+ "view.graphEmpty": "Nothing here that you may see.",
122
+ "view.graphFailed": "The graph could not be loaded.",
65
123
  "knowledge.graphZoomIn": "Zoom in",
66
124
  "knowledge.graphZoomOut": "Zoom out",
67
125
  "knowledge.graphReset": "Fit graph",
68
- "knowledge.graphFailed": "The authorized knowledge graph could not be loaded.",
69
126
  "knowledge.operationFailed": "The change was not saved. Reload the latest version and try again.",
70
127
  "knowledge.loadFailed": "This item could not be loaded. Check your access and try again.",
71
128
  "knowledge.downloadFailed": "The attachment could not be downloaded. Check your access and try again.",
72
129
  "knowledge.links": "Links and backlinks",
73
130
  "knowledge.noLinks": "No explicit relationships yet.",
74
- "knowledge.linkTarget": "Link to",
75
- "knowledge.selectLinkTarget": "Select knowledge",
76
- "knowledge.relation": "Relationship",
77
- "knowledge.relation.related": "Related to",
78
- "knowledge.relation.references": "References",
79
- "knowledge.relation.depends_on": "Depends on",
80
- "knowledge.relation.implements": "Implements",
81
- "knowledge.createLink": "Create relationship",
82
- "knowledge.deleteLink": "Delete relationship",
131
+ "knowledge.linksHelp": "Relationships are written in the text. Link a document from the editor with the / command; this list shows the result and who points here.",
132
+ "knowledge.linkOutgoing": "This document links to it",
133
+ "knowledge.linkIncoming": "It links to this document",
134
+ "knowledge.link.insert": "Link a document",
135
+ "knowledge.link.group": "Knowledge",
136
+ "knowledge.link.search": "Search documents you can see",
137
+ "knowledge.link.noMatches": "No document of yours matches.",
138
+ "knowledge.link.unresolved": "Unavailable document",
83
139
  "knowledge.saveConflict": "This document changed elsewhere. Reload it before saving again.",
84
140
  "knowledge.saveError": "This document could not be saved. Reload and try again.",
85
- "tools.connect": "Connect the portal",
86
- "tools.reconnect": "Reconnect the portal",
87
- "tools.disconnected": "Portal not connected",
88
- "tools.disconnectedHelp": "Sign in to the company MCP portal once. The portal decides which servers you may use and holds their credentials; Intel never sees them.",
141
+ "tools.signingIn": "Signing you in to the company portal",
142
+ "tools.noAccess": "No access to the company portal",
143
+ "tools.noAccessHelp": "Your Intel account does not reach the company MCP portal, so there is nothing to show here. An administrator decides in the portal who may use which server; ask them to include you.",
89
144
  "tools.empty": "No tools available to you",
90
145
  "tools.emptyHelp": "The portal answered, and it offers your account no tools. An administrator decides in the portal which servers exist and who may reach them.",
91
146
  "tools.unreachable": "Portal not reachable",
@@ -96,39 +151,99 @@
96
151
  "tools.outputSchema": "Result schema",
97
152
  "tools.destructiveShort": "Destructive",
98
153
  "tools.liveNote": "This list is a live query with your own portal access. Intel stores neither the tools nor who may use them; the portal decides both.",
99
- "tools.connectFailed": "The portal could not be connected with your current access.",
100
154
  "tools.arguments": "Arguments",
101
155
  "tools.invalidJson": "The arguments must be a JSON object in curly braces; a list or a single value will not work.",
102
156
  "flows.select": "Select a flow or create one to open the visual editor.",
103
157
  "flows.inspect": "Select a node to edit its configuration.",
104
158
  "flows.publish": "Publish",
159
+ "flows.publishNothing": "Nothing new to publish",
160
+ "flows.publishNeeded": "Publish — no run can start until you do",
105
161
  "flows.run": "Start run",
106
- "flows.share": "Share flow",
107
- "flows.revokeShare": "Revoke flow access",
108
- "flows.organization": "Everyone in the organization",
109
162
  "flows.runStarted": "Durable run started",
110
- "flows.node.trigger": "Trigger",
163
+ "flows.addNode": "Add a step",
164
+ "flows.closePalette": "Close the step bar",
165
+ "flows.nodePalette": "Step types",
166
+ "flows.node.trigger": "Start",
167
+ "flows.startExists": "A flow has exactly one start, and this one already has it.",
111
168
  "flows.node.instruction": "Instruction",
112
169
  "flows.node.knowledge": "Knowledge",
113
170
  "flows.node.tool": "Tool",
114
171
  "flows.node.condition": "Condition",
115
172
  "flows.node.approval": "Approval",
173
+ "flows.node.subflow": "Sub-flow",
116
174
  "flows.node.output": "Output",
117
- "flows.nodeDescription": "Description",
118
175
  "flows.instruction": "Instruction for the AI or person",
176
+ "flows.instructionHint": "Conditions, checks and branches can be described here in words. You do not need a separate node for every small decision.",
119
177
  "flows.knowledgeIds": "Knowledge references",
120
178
  "flows.knowledgeEmpty": "Create a knowledge item before adding this step.",
121
179
  "flows.knowledgeQuery": "Semantic retrieval hint",
180
+ "flows.subflowTarget": "Flow to call",
181
+ "flows.selectSubflow": "Select a flow to call",
182
+ "flows.subflowRule": "A flow may call a flow in its own folder or below it, or one in a folder every user may run. Publishing names the reason if it may not.",
183
+ "flows.subflowVersion": "Version to call",
184
+ "flows.subflowVersion.latest": "Latest — frozen when this flow is published",
185
+ "flows.subflowVersion.follows": "Always latest — this call runs along",
186
+ "flows.subflowVersion.pinned": "Frozen version",
187
+ "flows.subflowVersionHint.latest": "Publishing replaces this with the version the called flow has published then, so a later change to it leaves this flow alone.",
188
+ "flows.subflowVersionHint.follows": "This call takes whatever the called flow has published at the moment it runs. A change there changes this flow too.",
189
+ "flows.subflowVersionHint.pinned": "This call keeps running the version it was frozen to. Switch it back to latest to pick up the called flow's next publication.",
190
+ "flows.subflowBadge.follows": "runs along",
191
+ "flows.subflowBadge.pinned": "frozen",
192
+ "flows.publishPreview": "Before publishing",
193
+ "flows.publishPreviewIntro": "Publishing freezes every call marked below to the version it names. A call set to always latest stays as it is.",
194
+ "flows.publishPreviewEmpty": "This flow calls no other flow, so publishing freezes nothing.",
195
+ "flows.publishPreviewFreeze": "freezes to version {sequence}",
196
+ "flows.publishPreviewPinned": "already frozen to version {sequence}",
197
+ "flows.publishPreviewFollows": "runs along with the called flow",
198
+ "flows.publishPreviewUnavailable": "The called flow has published nothing yet, so this cannot be frozen.",
199
+ "flows.publishConfirm": "Publish",
200
+ "flows.publishFailed": "Publishing failed. Reload the latest version and try again.",
122
201
  "flows.tool": "MCP tool",
123
202
  "flows.selectTool": "Select a discovered tool",
124
203
  "flows.operationFailed": "The flow operation failed. Reload the latest version and try again.",
125
- "flows.shareFailed": "Flow access was not changed. Check your permission and try again.",
204
+ "flows.needs": "What this flow needs",
205
+ "flows.needsHint": "Read out of the graph. Whether a given person may reach any of it is decided when they run it.",
206
+ "flows.needsKnowledge": "Documents",
207
+ "flows.needsTools": "Tools",
208
+ "flows.needsHidden": "{count} more you cannot see",
209
+ "flows.needsEmpty": "This flow reads no documents and calls no tools.",
210
+ "flows.needsFailed": "What this flow needs could not be loaded.",
211
+ "runs.title": "Runs",
212
+ "runs.onlyFailed": "Only failed runs",
213
+ "runs.empty": "This flow has not run yet.",
214
+ "runs.emptyFailed": "No run of this flow has failed.",
215
+ "runs.more": "Show older runs",
216
+ "runs.stillRunning": "still running",
217
+ "runs.failedAt": "Failed at {step}:",
218
+ "runs.failedToLoad": "The runs of this flow could not be loaded.",
219
+ "runs.stepsFailedToLoad": "The steps of this run could not be loaded.",
220
+ "runs.stepsEmpty": "This run has not completed a step yet.",
221
+ "runs.stepsTitle": "Steps",
222
+ "runs.durationMs": "{value} ms",
223
+ "runs.durationSeconds": "{value} s",
224
+ "runs.durationMinutes": "{value} min",
225
+ "runs.status.queued": "Queued",
226
+ "runs.status.running": "Running",
227
+ "runs.status.waiting": "Waiting",
228
+ "runs.status.completed": "Completed",
229
+ "runs.status.failed": "Failed",
230
+ "runs.status.cancelled": "Cancelled",
231
+ "runs.trigger.manual": "started by hand",
232
+ "runs.trigger.subflow": "called by another flow",
233
+ "runs.outcome.completed": "done",
234
+ "runs.outcome.failed": "failed",
126
235
  "auth.signOut": "Sign out",
127
236
  "auth.signOutFailed": "Signing out failed. Check your connection and try again.",
128
237
  "common.title": "Title",
129
238
  "common.create": "Create",
130
239
  "common.save": "Save",
131
240
  "common.saving": "Saving…",
241
+ "common.saved": "Saved",
242
+ "common.saveDirty": "Save unsaved changes",
243
+ "common.unsavedTitle": "Unsaved changes",
244
+ "common.unsavedBody": "This has changes that have not been saved. Leaving now discards them.",
245
+ "common.unsavedStay": "Stay and save",
246
+ "common.unsavedLeave": "Leave without saving",
132
247
  "common.loading": "Loading…",
133
248
  "common.retry": "Try again",
134
249
  "common.close": "Close",