@anchrd/intel-ui 0.4.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/app/action-slot/action-slot.tsx +23 -0
  3. package/src/app/app-sidebar/app-sidebar.tsx +39 -24
  4. package/src/app/app-tree/app-tree.tsx +431 -60
  5. package/src/app/app.tsx +17 -2
  6. package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +2 -1
  7. package/src/app/tree-move/tree-move.tsx +197 -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 +120 -54
  15. package/src/data/intel-data-provider/intel-data-provider.types.ts +47 -12
  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 +670 -271
  19. package/src/flows/node-icon/node-icon.ts +28 -0
  20. package/src/flows/node-palette/node-palette.tsx +174 -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 +141 -24
  24. package/src/knowledge/knowledge.tsx +69 -355
  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 +129 -0
  29. package/src/main.tsx +2 -2
  30. package/src/resource-menu/resource-menu.tsx +580 -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/tools/tools.tsx +3 -3
  36. 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,174 @@
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 { useIntelRouterContext } from "@/router/router-context.ts";
5
+ import type { NodePaletteProps, PaletteKind } from "./node-palette.types.ts";
6
+
7
+ const storageKey = "intel.flow-palette";
8
+
9
+ // ⚠️ Reading `localStorage` throws outright in a few privacy modes, so the guard is a try, not a
10
+ // typeof check — the same reason `sidebar-preferences` guards it that way. Losing the preference is
11
+ // acceptable; losing the editor is not.
12
+ function readOpen(): boolean {
13
+ try {
14
+ return typeof window !== "undefined" && window.localStorage.getItem(storageKey) === "open";
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function writeOpen(open: boolean): void {
21
+ try {
22
+ if (typeof window !== "undefined")
23
+ window.localStorage.setItem(storageKey, open ? "open" : "closed");
24
+ } catch {}
25
+ }
26
+
27
+ // Closed is the default, and the preference is written through: the bar is chrome, exactly like the
28
+ // sidebar whose open state already survives a reload. Someone who builds flows all day would
29
+ // otherwise re-open it on every load, and the ticket's own reason for collapsing it — the canvas
30
+ // belongs to the graph — is just as true for someone who never opens it.
31
+ export function usePaletteOpen(): [boolean, (open: boolean) => void] {
32
+ const [open, setOpen] = useState(readOpen);
33
+ return [
34
+ open,
35
+ (next: boolean) => {
36
+ setOpen(next);
37
+ writeOpen(next);
38
+ },
39
+ ];
40
+ }
41
+
42
+ export function NodePalette<K extends PaletteKind>({
43
+ kinds,
44
+ open,
45
+ setOpen,
46
+ add,
47
+ disabledReason,
48
+ }: NodePaletteProps<K>) {
49
+ const { i18n } = useIntelRouterContext();
50
+ const listId = useId();
51
+ const triggerRef = useRef<HTMLButtonElement>(null);
52
+ const listRef = useRef<HTMLDivElement>(null);
53
+ // A toolbar carries one tab stop, so the group is entered with a single Tab and left with the
54
+ // next one instead of costing seven.
55
+ const [active, setActive] = useState(0);
56
+
57
+ // The focus goes back where it came from, so closing does not drop the caret at the top of the
58
+ // document and make the next Tab start over from the page.
59
+ function close() {
60
+ setOpen(false);
61
+ triggerRef.current?.focus();
62
+ }
63
+
64
+ function entries(): HTMLButtonElement[] {
65
+ return [
66
+ ...(listRef.current?.querySelectorAll<HTMLButtonElement>("button[data-palette-item]") ?? []),
67
+ ];
68
+ }
69
+
70
+ // A disabled entry is stepped over rather than focused: its reason is already on the entry, and
71
+ // stopping there would only be a dead end for the keyboard.
72
+ function focusEntry(from: number, delta: number) {
73
+ const items = entries();
74
+ if (items.length === 0) return;
75
+ for (let step = 1; step <= items.length; step += 1) {
76
+ const index = (((from + delta * step) % items.length) + items.length) % items.length;
77
+ const item = items[index];
78
+ if (item && !item.disabled) {
79
+ setActive(index);
80
+ item.focus();
81
+ return;
82
+ }
83
+ }
84
+ }
85
+
86
+ function onListKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
87
+ if (event.key === "Escape") {
88
+ event.stopPropagation();
89
+ close();
90
+ return;
91
+ }
92
+ const forward = event.key === "ArrowDown" || event.key === "ArrowRight";
93
+ const backward = event.key === "ArrowUp" || event.key === "ArrowLeft";
94
+ if (!forward && !backward) return;
95
+ event.preventDefault();
96
+ const items = entries();
97
+ const current = items.indexOf(document.activeElement as HTMLButtonElement);
98
+ focusEntry(current < 0 ? 0 : current, forward ? 1 : -1);
99
+ }
100
+
101
+ function onTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
102
+ if (!open) return;
103
+ if (event.key === "Escape") {
104
+ event.stopPropagation();
105
+ close();
106
+ return;
107
+ }
108
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
109
+ event.preventDefault();
110
+ focusEntry(-1, 1);
111
+ }
112
+ }
113
+
114
+ // ⚠️ `w-max` below: an absolutely positioned box shrink-wraps its widest child, and collapsed that
115
+ // is the 36px trigger. Without it the open bar inherits those 36px as its own maximum and folds
116
+ // seven entries into a single column — the flex row wraps after every item.
117
+ return (
118
+ <div className="absolute left-4 top-4 z-10 w-max max-w-[calc(100%-2rem)]">
119
+ <button
120
+ ref={triggerRef}
121
+ type="button"
122
+ aria-expanded={open}
123
+ aria-controls={listId}
124
+ onClick={() => (open ? close() : setOpen(true))}
125
+ onKeyDown={onTriggerKeyDown}
126
+ className="inline-flex size-9 items-center justify-center rounded-lg border bg-card/95 text-card-foreground shadow-sm outline-none backdrop-blur hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
127
+ >
128
+ {open ? (
129
+ <X aria-hidden="true" className="size-4" />
130
+ ) : (
131
+ <Plus aria-hidden="true" className="size-4" />
132
+ )}
133
+ <span className="sr-only">{i18n.t(open ? "flows.closePalette" : "flows.addNode")}</span>
134
+ </button>
135
+ {/* Over the canvas rather than pushing it: the graph is a spatial workspace, and resizing the
136
+ viewport would shift every node under the pointer each time the bar opens. It is dismissed
137
+ instead — the trigger, Escape, or a click on the canvas. */}
138
+ {open && (
139
+ <div
140
+ ref={listRef}
141
+ id={listId}
142
+ role="toolbar"
143
+ aria-orientation="horizontal"
144
+ aria-label={i18n.t("flows.nodePalette")}
145
+ onKeyDown={onListKeyDown}
146
+ className="mt-2 flex max-w-full flex-wrap gap-1 rounded-lg border bg-card/95 p-2 shadow-sm backdrop-blur"
147
+ >
148
+ {kinds.map((kind, index) => {
149
+ const Icon: NodeIcon = nodeIcon[kind];
150
+ const reason = disabledReason?.(kind) ?? null;
151
+ return (
152
+ <button
153
+ key={kind}
154
+ type="button"
155
+ data-palette-item=""
156
+ disabled={reason !== null}
157
+ title={reason ?? undefined}
158
+ tabIndex={index === active ? 0 : -1}
159
+ onClick={() => {
160
+ setActive(index);
161
+ add(kind);
162
+ }}
163
+ className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 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"
164
+ >
165
+ <Icon aria-hidden="true" className="size-3.5" />
166
+ {i18n.t(`flows.node.${kind}`)}
167
+ </button>
168
+ );
169
+ })}
170
+ </div>
171
+ )}
172
+ </div>
173
+ );
174
+ }
@@ -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,33 +92,50 @@
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
141
  "tools.connect": "Connect the portal",
@@ -102,33 +158,94 @@
102
158
  "flows.select": "Select a flow or create one to open the visual editor.",
103
159
  "flows.inspect": "Select a node to edit its configuration.",
104
160
  "flows.publish": "Publish",
161
+ "flows.publishNothing": "Nothing new to publish",
162
+ "flows.publishNeeded": "Publish — no run can start until you do",
105
163
  "flows.run": "Start run",
106
- "flows.share": "Share flow",
107
- "flows.revokeShare": "Revoke flow access",
108
- "flows.organization": "Everyone in the organization",
109
164
  "flows.runStarted": "Durable run started",
110
- "flows.node.trigger": "Trigger",
165
+ "flows.addNode": "Add a step",
166
+ "flows.closePalette": "Close the step bar",
167
+ "flows.nodePalette": "Step types",
168
+ "flows.node.trigger": "Start",
169
+ "flows.startExists": "A flow has exactly one start, and this one already has it.",
111
170
  "flows.node.instruction": "Instruction",
112
171
  "flows.node.knowledge": "Knowledge",
113
172
  "flows.node.tool": "Tool",
114
173
  "flows.node.condition": "Condition",
115
174
  "flows.node.approval": "Approval",
175
+ "flows.node.subflow": "Sub-flow",
116
176
  "flows.node.output": "Output",
117
- "flows.nodeDescription": "Description",
118
177
  "flows.instruction": "Instruction for the AI or person",
178
+ "flows.instructionHint": "Conditions, checks and branches can be described here in words. You do not need a separate node for every small decision.",
119
179
  "flows.knowledgeIds": "Knowledge references",
120
180
  "flows.knowledgeEmpty": "Create a knowledge item before adding this step.",
121
181
  "flows.knowledgeQuery": "Semantic retrieval hint",
182
+ "flows.subflowTarget": "Flow to call",
183
+ "flows.selectSubflow": "Select a flow to call",
184
+ "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.",
185
+ "flows.subflowVersion": "Version to call",
186
+ "flows.subflowVersion.latest": "Latest — frozen when this flow is published",
187
+ "flows.subflowVersion.follows": "Always latest — this call runs along",
188
+ "flows.subflowVersion.pinned": "Frozen version",
189
+ "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.",
190
+ "flows.subflowVersionHint.follows": "This call takes whatever the called flow has published at the moment it runs. A change there changes this flow too.",
191
+ "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.",
192
+ "flows.subflowBadge.follows": "runs along",
193
+ "flows.subflowBadge.pinned": "frozen",
194
+ "flows.publishPreview": "Before publishing",
195
+ "flows.publishPreviewIntro": "Publishing freezes every call marked below to the version it names. A call set to always latest stays as it is.",
196
+ "flows.publishPreviewEmpty": "This flow calls no other flow, so publishing freezes nothing.",
197
+ "flows.publishPreviewFreeze": "freezes to version {sequence}",
198
+ "flows.publishPreviewPinned": "already frozen to version {sequence}",
199
+ "flows.publishPreviewFollows": "runs along with the called flow",
200
+ "flows.publishPreviewUnavailable": "The called flow has published nothing yet, so this cannot be frozen.",
201
+ "flows.publishConfirm": "Publish",
202
+ "flows.publishFailed": "Publishing failed. Reload the latest version and try again.",
122
203
  "flows.tool": "MCP tool",
123
204
  "flows.selectTool": "Select a discovered tool",
124
205
  "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.",
206
+ "flows.needs": "What this flow needs",
207
+ "flows.needsHint": "Read out of the graph. Whether a given person may reach any of it is decided when they run it.",
208
+ "flows.needsKnowledge": "Documents",
209
+ "flows.needsTools": "Tools",
210
+ "flows.needsHidden": "{count} more you cannot see",
211
+ "flows.needsEmpty": "This flow reads no documents and calls no tools.",
212
+ "flows.needsFailed": "What this flow needs could not be loaded.",
213
+ "runs.title": "Runs",
214
+ "runs.onlyFailed": "Only failed runs",
215
+ "runs.empty": "This flow has not run yet.",
216
+ "runs.emptyFailed": "No run of this flow has failed.",
217
+ "runs.more": "Show older runs",
218
+ "runs.stillRunning": "still running",
219
+ "runs.failedAt": "Failed at {step}:",
220
+ "runs.failedToLoad": "The runs of this flow could not be loaded.",
221
+ "runs.stepsFailedToLoad": "The steps of this run could not be loaded.",
222
+ "runs.stepsEmpty": "This run has not completed a step yet.",
223
+ "runs.stepsTitle": "Steps",
224
+ "runs.durationMs": "{value} ms",
225
+ "runs.durationSeconds": "{value} s",
226
+ "runs.durationMinutes": "{value} min",
227
+ "runs.status.queued": "Queued",
228
+ "runs.status.running": "Running",
229
+ "runs.status.waiting": "Waiting",
230
+ "runs.status.completed": "Completed",
231
+ "runs.status.failed": "Failed",
232
+ "runs.status.cancelled": "Cancelled",
233
+ "runs.trigger.manual": "started by hand",
234
+ "runs.trigger.subflow": "called by another flow",
235
+ "runs.outcome.completed": "done",
236
+ "runs.outcome.failed": "failed",
126
237
  "auth.signOut": "Sign out",
127
238
  "auth.signOutFailed": "Signing out failed. Check your connection and try again.",
128
239
  "common.title": "Title",
129
240
  "common.create": "Create",
130
241
  "common.save": "Save",
131
242
  "common.saving": "Saving…",
243
+ "common.saved": "Saved",
244
+ "common.saveDirty": "Save unsaved changes",
245
+ "common.unsavedTitle": "Unsaved changes",
246
+ "common.unsavedBody": "This has changes that have not been saved. Leaving now discards them.",
247
+ "common.unsavedStay": "Stay and save",
248
+ "common.unsavedLeave": "Leave without saving",
132
249
  "common.loading": "Loading…",
133
250
  "common.retry": "Try again",
134
251
  "common.close": "Close",