@anchrd/intel-ui 0.18.0 → 0.19.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/package.json +2 -9
- package/src/app/app-tree/app-tree.tsx +59 -147
- package/src/app/settings-dialog/settings-dialog.tsx +10 -32
- package/src/data/intel-data-provider/intel-data-provider.ts +0 -163
- package/src/data/intel-data-provider/intel-data-provider.types.ts +0 -100
- package/src/entry-picker/entry-picker.tsx +6 -6
- package/src/i18n/de.json +6 -217
- package/src/i18n/en.json +6 -217
- package/src/i18n/es.json +6 -217
- package/src/import-bundle/import-bundle.tsx +152 -0
- package/src/kind-icon.ts +1 -15
- package/src/main.tsx +7 -10
- package/src/node-table/node-table.tsx +2 -2
- package/src/nodes/nodes.tsx +0 -34
- package/src/resource-menu/resource-menu.tsx +42 -14
- package/src/section-hint/section-hint.tsx +2 -2
- package/src/title-row/title-row.tsx +5 -22
- package/src/tools/tools.tsx +0 -2
- package/vite.config.ts +0 -4
- package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
- package/src/agent/agent-calendar/agent-calendar.tsx +0 -160
- package/src/agent/agent-chat/agent-chat.tsx +0 -118
- package/src/agent/agent-costs/agent-costs.ts +0 -95
- package/src/agent/agent-cron/agent-cron.ts +0 -68
- package/src/agent/agent-definition/agent-definition.ts +0 -96
- package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
- package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
- package/src/agent/agent-log/agent-log.tsx +0 -308
- package/src/agent/agent-models/agent-models.ts +0 -166
- package/src/agent/agent-models/agent-models.types.ts +0 -24
- package/src/agent/agent-profile/agent-profile.tsx +0 -1006
- package/src/agent/agent-state/agent-state.ts +0 -85
- package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
- package/src/agent/agent.tsx +0 -507
- package/src/board/board-calendar/board-calendar.tsx +0 -89
- package/src/board/board-card/board-card.tsx +0 -106
- package/src/board/board-data/board-data.ts +0 -241
- package/src/board/board-data/board-data.types.ts +0 -63
- package/src/board/board-detail/board-detail.tsx +0 -629
- package/src/board/board-gantt/board-gantt.ts +0 -545
- package/src/board/board-gantt/board-gantt.tsx +0 -286
- package/src/board/board-graph/board-graph.ts +0 -174
- package/src/board/board-graph/board-graph.tsx +0 -168
- package/src/board/board-items/board-items.ts +0 -183
- package/src/board/board-kanban/board-kanban.ts +0 -137
- package/src/board/board-kanban/board-kanban.tsx +0 -257
- package/src/board/board-status/board-status.ts +0 -59
- package/src/board/board-statuses/board-statuses.ts +0 -63
- package/src/board/board-statuses/board-statuses.tsx +0 -228
- package/src/board/board-table/board-table.ts +0 -33
- package/src/board/board-table/board-table.tsx +0 -413
- package/src/board/board-views/board-views.tsx +0 -68
- package/src/board/board-views/board-views.types.ts +0 -29
- package/src/board/board.tsx +0 -251
- package/src/components/ui/item-calendar.tsx +0 -181
- package/src/components/ui/item-gantt.tsx +0 -463
- package/src/components/ui/kanban.tsx +0 -282
- package/src/data/agent-runtime/agent-runtime.ts +0 -131
- package/src/hooks/use-capabilities.ts +0 -22
- package/src/hooks/use-model-catalog.ts +0 -26
- package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
- package/src/timezone/timezone-context.tsx +0 -65
- package/src/timezone/timezone.ts +0 -174
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { zipSync } from "fflate";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { treeLevelKey } from "@/app/tree-move/tree-move.tsx";
|
|
6
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
7
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The reading half of the bundle round trip (#136/#137), as one seam two menus share.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ Import is offered in two places and they are not alternatives: the three-dot menu of a folder
|
|
13
|
+
* files into that folder, and the tree's root button is the ONLY way to reach the top level — a
|
|
14
|
+
* three-dot menu always has a resource, and the root is not one (#346). Both need the same file
|
|
15
|
+
* inputs, the same 64 MB refusal and the same invalidations, and a second copy of that is how the
|
|
16
|
+
* two start disagreeing about what a successful import makes stale.
|
|
17
|
+
*/
|
|
18
|
+
export type ImportKind = "importZip" | "importFolder";
|
|
19
|
+
|
|
20
|
+
// The server refuses anything larger anyway (#137); refusing here saves uploading it first.
|
|
21
|
+
const MaxImportBytes = 64 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
// A picked directory as the zip the import endpoint reads. `webkitRelativePath` carries the folder
|
|
24
|
+
// structure, with the picked folder itself as the first segment — kept, so the folder lands as a
|
|
25
|
+
// folder rather than spilling its contents into the target.
|
|
26
|
+
async function zipOfDirectory(files: readonly File[]): Promise<Blob> {
|
|
27
|
+
const entries: Record<string, Uint8Array> = {};
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
const path = file.webkitRelativePath || file.name;
|
|
31
|
+
total += file.size;
|
|
32
|
+
if (total > MaxImportBytes) throw new Error("Import exceeds the 64 MB limit");
|
|
33
|
+
entries[path] = new Uint8Array(await file.arrayBuffer());
|
|
34
|
+
}
|
|
35
|
+
return new Blob([zipSync(entries).slice().buffer], { type: "application/zip" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BundleImport {
|
|
39
|
+
/** Opens the file picker for `kind` and files whatever comes back into `parentId`. */
|
|
40
|
+
start(parentId: string | null, kind: ImportKind): void;
|
|
41
|
+
/**
|
|
42
|
+
* The two hidden file pickers. Render once inside the component that called the hook.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ They portal to `document.body` rather than landing where they are written. A file input is
|
|
45
|
+
* invisible but still an element, and the three-dot menu sits at the end of a title line whose
|
|
46
|
+
* rule is that the menu is the LAST thing in it (#53) — an input after it breaks that rule for
|
|
47
|
+
* nobody's benefit, because a hidden picker has no place on screen to be in the first place.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ `tabIndex={-1}`, and that is not a detail. The folder table renders one `ResourceMenu` PER
|
|
50
|
+
* ROW, so a folder holding twenty folders holds forty of these — and `sr-only` hides an element
|
|
51
|
+
* from the eye, never from the tab order. Without it, tabbing through such a screen walks through
|
|
52
|
+
* forty stops that show nothing, and #346 asks every entry to have a VISIBLE focus. The control
|
|
53
|
+
* that opens a picker is the menu item; the input is the mechanism behind it and is clicked by
|
|
54
|
+
* `start`, never by a person.
|
|
55
|
+
*/
|
|
56
|
+
inputs: React.ReactNode;
|
|
57
|
+
isError: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useBundleImport(options: {
|
|
61
|
+
/**
|
|
62
|
+
* Where this pair of pickers files, in words — a folder title, or the name of the top level.
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ Not decoration. Both callers render their inputs into `document.body`, so on a folder screen
|
|
65
|
+
* there are two of each in the document at once: the tree's (which files at the top level) and
|
|
66
|
+
* this menu's. `sr-only` keeps them out of sight but not out of the accessibility tree or the tab
|
|
67
|
+
* order, and two controls called "Import zip bundle" are two controls a reader cannot tell apart
|
|
68
|
+
* — nor, as the first browser run of #388 showed, can anything else looking one up by name.
|
|
69
|
+
*/
|
|
70
|
+
where: string;
|
|
71
|
+
onImported?(parentId: string | null): void | Promise<void>;
|
|
72
|
+
}): BundleImport {
|
|
73
|
+
const { data } = useIntelRouterContext();
|
|
74
|
+
const i18n = useI18n();
|
|
75
|
+
const queryClient = useQueryClient();
|
|
76
|
+
const [importTo, setImportTo] = useState<string | null>(null);
|
|
77
|
+
const zipInput = useRef<HTMLInputElement>(null);
|
|
78
|
+
const folderInput = useRef<HTMLInputElement>(null);
|
|
79
|
+
|
|
80
|
+
// One import, one request, one answer (#137): the server creates the whole subtree or nothing, so
|
|
81
|
+
// this mutation never has a partial state to clean up after — unlike the attachment upload, whose
|
|
82
|
+
// two steps can strand a node.
|
|
83
|
+
const importBundle = useMutation({
|
|
84
|
+
mutationFn: async ({ parentId, zip }: { parentId: string | null; zip: Blob }) => {
|
|
85
|
+
if (zip.size > MaxImportBytes) throw new Error("Import exceeds the 64 MB limit");
|
|
86
|
+
const result = await data.importNodeBundle({
|
|
87
|
+
nodeId: parentId,
|
|
88
|
+
zip,
|
|
89
|
+
idempotencyKey: crypto.randomUUID(),
|
|
90
|
+
});
|
|
91
|
+
return { parentId, result };
|
|
92
|
+
},
|
|
93
|
+
onSuccess: async (imported) => {
|
|
94
|
+
await options.onImported?.(imported.parentId);
|
|
95
|
+
await Promise.all([
|
|
96
|
+
queryClient.invalidateQueries({ queryKey: treeLevelKey(imported.parentId) }),
|
|
97
|
+
queryClient.invalidateQueries({ queryKey: ["node-graph"] }),
|
|
98
|
+
// The graph view of that same level draws exactly this list (#19).
|
|
99
|
+
queryClient.invalidateQueries({ queryKey: ["relation-graph"] }),
|
|
100
|
+
// A bundle can carry flows, so the flow collections go stale alongside the node ones.
|
|
101
|
+
queryClient.invalidateQueries({ queryKey: ["flows"] }),
|
|
102
|
+
]);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
start(parentId, kind) {
|
|
108
|
+
setImportTo(parentId);
|
|
109
|
+
(kind === "importZip" ? zipInput : folderInput).current?.click();
|
|
110
|
+
},
|
|
111
|
+
isError: importBundle.isError,
|
|
112
|
+
inputs: createPortal(
|
|
113
|
+
<>
|
|
114
|
+
<input
|
|
115
|
+
ref={zipInput}
|
|
116
|
+
type="file"
|
|
117
|
+
tabIndex={-1}
|
|
118
|
+
accept=".zip,application/zip"
|
|
119
|
+
className="sr-only"
|
|
120
|
+
aria-label={i18n.t("tree.importZipInto", { where: options.where })}
|
|
121
|
+
onChange={(event) => {
|
|
122
|
+
const file = event.target.files?.[0];
|
|
123
|
+
if (file) importBundle.mutate({ parentId: importTo, zip: file });
|
|
124
|
+
event.currentTarget.value = "";
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
{/* `webkitdirectory` is the one way a browser hands over a folder; React does not know the
|
|
128
|
+
attribute, so it is spread past the type checker rather than invented as a prop. */}
|
|
129
|
+
<input
|
|
130
|
+
ref={folderInput}
|
|
131
|
+
type="file"
|
|
132
|
+
tabIndex={-1}
|
|
133
|
+
multiple
|
|
134
|
+
{...({ webkitdirectory: "" } as object)}
|
|
135
|
+
className="sr-only"
|
|
136
|
+
aria-label={i18n.t("tree.importFolderInto", { where: options.where })}
|
|
137
|
+
onChange={(event) => {
|
|
138
|
+
const files = Array.from(event.target.files ?? []);
|
|
139
|
+
const parentId = importTo;
|
|
140
|
+
if (files.length > 0) {
|
|
141
|
+
void zipOfDirectory(files)
|
|
142
|
+
.then((zip) => importBundle.mutate({ parentId, zip }))
|
|
143
|
+
.catch(() => importBundle.mutate({ parentId, zip: new Blob([]) }));
|
|
144
|
+
}
|
|
145
|
+
event.currentTarget.value = "";
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</>,
|
|
149
|
+
document.body,
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/kind-icon.ts
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Bot,
|
|
3
|
-
FileText,
|
|
4
|
-
Folder,
|
|
5
|
-
KanbanSquare,
|
|
6
|
-
type LucideIcon,
|
|
7
|
-
Paperclip,
|
|
8
|
-
Table,
|
|
9
|
-
Workflow,
|
|
10
|
-
} from "lucide-react";
|
|
1
|
+
import { FileText, Folder, type LucideIcon, Paperclip, Table, Workflow } from "lucide-react";
|
|
11
2
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
12
3
|
|
|
13
4
|
/**
|
|
@@ -24,10 +15,5 @@ export const kindIcons: Record<TreeEntry["kind"], LucideIcon> = {
|
|
|
24
15
|
document: FileText,
|
|
25
16
|
attachment: Paperclip,
|
|
26
17
|
table: Table,
|
|
27
|
-
// An agent is listed and named like any other row (#139), and since #143 opening one leads to
|
|
28
|
-
// its own screen. The sidebar's create menu draws this same icon rather than naming `Bot` again.
|
|
29
|
-
agent: Bot,
|
|
30
|
-
// A board is a row like any other (#285); the views that open one are anchrd/intel#286.
|
|
31
|
-
board: KanbanSquare,
|
|
32
18
|
flow: Workflow,
|
|
33
19
|
};
|
package/src/main.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import { createIntelRouter } from "@/router/router.tsx";
|
|
|
13
13
|
import { SignInRefused } from "@/sign-in-refused/sign-in-refused.tsx";
|
|
14
14
|
import { applyTheme, readThemeChoice, SystemThemeQuery, themeFor } from "@/theme/theme.ts";
|
|
15
15
|
import { ThemeProvider } from "@/theme/theme-context.tsx";
|
|
16
|
-
import { TimezoneProvider } from "@/timezone/timezone-context.tsx";
|
|
17
16
|
import "./styles.css";
|
|
18
17
|
import "./theme/custom.css";
|
|
19
18
|
|
|
@@ -55,15 +54,13 @@ function paint(): void {
|
|
|
55
54
|
place for it. */}
|
|
56
55
|
<I18nProvider languages={languages}>
|
|
57
56
|
<ThemeProvider store={window.localStorage}>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)}
|
|
66
|
-
</TimezoneProvider>
|
|
57
|
+
{refused ? (
|
|
58
|
+
<SignInRefused />
|
|
59
|
+
) : (
|
|
60
|
+
<QueryClientProvider client={queryClient}>
|
|
61
|
+
<RouterProvider router={router} />
|
|
62
|
+
</QueryClientProvider>
|
|
63
|
+
)}
|
|
67
64
|
</ThemeProvider>
|
|
68
65
|
</I18nProvider>
|
|
69
66
|
</StrictMode>,
|
|
@@ -9,8 +9,8 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
|
9
9
|
* A table as a grid (#40).
|
|
10
10
|
*
|
|
11
11
|
* Reading is the whole of the first cut: there is no cell editor, and rows arrive through
|
|
12
|
-
* `node_table_append` — the point of the kind is that an
|
|
13
|
-
* retyping anything.
|
|
12
|
+
* `node_table_append` — the point of the kind is that a flow or an MCP client fills it without a
|
|
13
|
+
* person retyping anything. Editing in the grid is anchrd/intel#165.
|
|
14
14
|
*
|
|
15
15
|
* ⚠️ The rows are the server's answer, parsed by the server's one CSV reader. A second reader here
|
|
16
16
|
* would eventually disagree with it about a quoted comma, and the grid would then show something
|
package/src/nodes/nodes.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
4
4
|
import { Download, History, Paperclip } from "lucide-react";
|
|
5
5
|
import { lazy, Suspense, useState } from "react";
|
|
6
|
-
import { AgentAvatar } from "@/agent/agent-avatar/agent-avatar.tsx";
|
|
7
6
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
8
7
|
import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
|
|
9
8
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
@@ -18,17 +17,6 @@ import { TitleRow } from "@/title-row/title-row.tsx";
|
|
|
18
17
|
const NodeEditor = lazy(async () => ({
|
|
19
18
|
default: (await import("@/node-editor/node-editor.tsx")).NodeEditor,
|
|
20
19
|
}));
|
|
21
|
-
// Lazy for the same reason the editor is: assistant-ui and its stream client are the agent page's
|
|
22
|
-
// alone, and every other screen would otherwise pay for them on first paint.
|
|
23
|
-
const AgentPanel = lazy(async () => ({
|
|
24
|
-
default: (await import("@/agent/agent.tsx")).AgentPanel,
|
|
25
|
-
}));
|
|
26
|
-
// Lazy for the third time, and the reason is the same: dnd-kit, TanStack Table and — behind its own
|
|
27
|
-
// second split — Sigma belong to the board alone, and every other screen would otherwise carry them
|
|
28
|
-
// into its first paint.
|
|
29
|
-
const BoardPanel = lazy(async () => ({
|
|
30
|
-
default: (await import("@/board/board.tsx")).BoardPanel,
|
|
31
|
-
}));
|
|
32
20
|
|
|
33
21
|
export function Nodes() {
|
|
34
22
|
const { data } = useIntelRouterContext();
|
|
@@ -121,8 +109,6 @@ export function Nodes() {
|
|
|
121
109
|
title={selected.title}
|
|
122
110
|
description={selected.description}
|
|
123
111
|
target={{ type: "node", node: selected }}
|
|
124
|
-
leading={selected.kind === "agent" ? <AgentAvatar title={selected.title} /> : null}
|
|
125
|
-
divider={selected.kind !== "agent"}
|
|
126
112
|
>
|
|
127
113
|
{selected.kind !== "folder" && (
|
|
128
114
|
<TooltipProvider delayDuration={300}>
|
|
@@ -157,26 +143,6 @@ export function Nodes() {
|
|
|
157
143
|
<AttachmentPanel node={selected} />
|
|
158
144
|
) : selected.kind === "table" ? (
|
|
159
145
|
<NodeTablePanel node={selected} />
|
|
160
|
-
) : selected.kind === "board" ? (
|
|
161
|
-
// ⚠️ Before the editor branch for the same reason the agent is: a board's version
|
|
162
|
-
// body is a JSON document (#285), and BlockNote would open it as prose.
|
|
163
|
-
<Suspense
|
|
164
|
-
fallback={
|
|
165
|
-
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
166
|
-
}
|
|
167
|
-
>
|
|
168
|
-
<BoardPanel key={selected.id} node={selected} />
|
|
169
|
-
</Suspense>
|
|
170
|
-
) : selected.kind === "agent" ? (
|
|
171
|
-
// ⚠️ Before the editor branch, and never reaching it: an agent's version body is a
|
|
172
|
-
// definition document, not prose, and BlockNote would try to open JSON as a text.
|
|
173
|
-
<Suspense
|
|
174
|
-
fallback={
|
|
175
|
-
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
176
|
-
}
|
|
177
|
-
>
|
|
178
|
-
<AgentPanel key={selected.id} node={selected} />
|
|
179
|
-
</Suspense>
|
|
180
146
|
) : document.data ? (
|
|
181
147
|
<Suspense
|
|
182
148
|
fallback={
|
|
@@ -2,7 +2,6 @@ import type {
|
|
|
2
2
|
Flow,
|
|
3
3
|
FlowValidation,
|
|
4
4
|
Node,
|
|
5
|
-
NodeKind,
|
|
6
5
|
ResourceVerb,
|
|
7
6
|
UnreadableNodes,
|
|
8
7
|
} from "@anchrd/intel-contract";
|
|
@@ -12,7 +11,9 @@ import {
|
|
|
12
11
|
Archive,
|
|
13
12
|
CornerLeftUp,
|
|
14
13
|
Ellipsis,
|
|
14
|
+
FileArchive,
|
|
15
15
|
FolderDown,
|
|
16
|
+
FolderUp,
|
|
16
17
|
Link2,
|
|
17
18
|
Pencil,
|
|
18
19
|
Share2,
|
|
@@ -27,11 +28,15 @@ import {
|
|
|
27
28
|
DropdownMenuContent,
|
|
28
29
|
DropdownMenuItem,
|
|
29
30
|
DropdownMenuSeparator,
|
|
31
|
+
DropdownMenuSub,
|
|
32
|
+
DropdownMenuSubContent,
|
|
33
|
+
DropdownMenuSubTrigger,
|
|
30
34
|
DropdownMenuTrigger,
|
|
31
35
|
} from "@/components/ui/dropdown-menu";
|
|
32
36
|
import { flowEntry } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
33
37
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
34
38
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
39
|
+
import { useBundleImport } from "@/import-bundle/import-bundle.tsx";
|
|
35
40
|
import { Modal } from "@/modal/modal.tsx";
|
|
36
41
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
37
42
|
import { selectedFrom } from "@/router/selection-search.ts";
|
|
@@ -81,18 +86,15 @@ function parentOfTarget(target: ResourceTarget): string | null {
|
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
/**
|
|
84
|
-
* Which sentence explains a verb
|
|
89
|
+
* Which sentence explains a verb.
|
|
85
90
|
*
|
|
86
|
-
* ⚠️
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* needs no special wording needs no entry.
|
|
91
|
+
* ⚠️ One sentence per verb, and that is only true again since Agents were parked (#388). "Run"
|
|
92
|
+
* used to mean two different things — starting the flows filed in a folder, and using an agent
|
|
93
|
+
* through its chat, its schedules and its MCP address — so #143 gave the agent its own wording.
|
|
94
|
+
* With one meaning left, a per-kind branch would be a fork nothing takes.
|
|
91
95
|
*/
|
|
92
|
-
export function verbHintKey(verb: ResourceVerb
|
|
93
|
-
return verb
|
|
94
|
-
? "node.verbHint.execute.agent"
|
|
95
|
-
: `node.verbHint.${verb}`;
|
|
96
|
+
export function verbHintKey(verb: ResourceVerb): string {
|
|
97
|
+
return `node.verbHint.${verb}`;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
// ⚠️ The same reading `moveErrorKey` does for a move, for the changes this menu makes. A conflict is
|
|
@@ -163,6 +165,10 @@ export function ResourceMenu({
|
|
|
163
165
|
// link anywhere. Both entries are therefore absent on one rather than offered and inert.
|
|
164
166
|
const retrievable = node !== null && node.kind !== "folder";
|
|
165
167
|
const linkable = retrievable;
|
|
168
|
+
// ⚠️ Absent, not disabled — the rule at the top of this file. An import needs a folder to file
|
|
169
|
+
// into; a document has nowhere to put a subtree, and a greyed-out entry would promise it does.
|
|
170
|
+
const importable = node !== null && node.kind === "folder";
|
|
171
|
+
const bundleImport = useBundleImport({ where: title });
|
|
166
172
|
|
|
167
173
|
// Everything a change here can make stale. The row sits in one level of the tree, the open screen
|
|
168
174
|
// reads the record, and the flat collections behind the search, the link picker and the relation
|
|
@@ -299,6 +305,26 @@ export function ResourceMenu({
|
|
|
299
305
|
<FolderDown aria-hidden="true" />
|
|
300
306
|
{i18n.t("resource.export")}
|
|
301
307
|
</DropdownMenuItem>
|
|
308
|
+
{/* The other half of the same round trip, next to it rather than in the tree's plus
|
|
309
|
+
(#346): one word in the menu, both sources under it. */}
|
|
310
|
+
{importable ? (
|
|
311
|
+
<DropdownMenuSub>
|
|
312
|
+
<DropdownMenuSubTrigger>
|
|
313
|
+
<FolderUp aria-hidden="true" />
|
|
314
|
+
{i18n.t("resource.import")}
|
|
315
|
+
</DropdownMenuSubTrigger>
|
|
316
|
+
<DropdownMenuSubContent>
|
|
317
|
+
<DropdownMenuItem onSelect={() => bundleImport.start(id, "importZip")}>
|
|
318
|
+
<FileArchive aria-hidden="true" />
|
|
319
|
+
{i18n.t("tree.new.importZip")}
|
|
320
|
+
</DropdownMenuItem>
|
|
321
|
+
<DropdownMenuItem onSelect={() => bundleImport.start(id, "importFolder")}>
|
|
322
|
+
<FolderUp aria-hidden="true" />
|
|
323
|
+
{i18n.t("tree.new.importFolder")}
|
|
324
|
+
</DropdownMenuItem>
|
|
325
|
+
</DropdownMenuSubContent>
|
|
326
|
+
</DropdownMenuSub>
|
|
327
|
+
) : null}
|
|
302
328
|
{/* Only a flow can be run, so only a flow can be asked whether it would. Rarely needed —
|
|
303
329
|
which is why it is in the menu and not in the title line (#72). */}
|
|
304
330
|
{node === null ? (
|
|
@@ -344,6 +370,10 @@ export function ResourceMenu({
|
|
|
344
370
|
{/* Its own sentence, not `resourceErrorKey`'s: nothing was changed, something failed to
|
|
345
371
|
arrive, and "the change was not saved" would send the reader looking for a change. */}
|
|
346
372
|
{exportBundle.isError ? <MenuFailure>{i18n.t("resource.exportFailed")}</MenuFailure> : null}
|
|
373
|
+
{/* The import's own sentence, and its own pickers — only where the entry exists, because two
|
|
374
|
+
file inputs on every document's menu would be two elements nothing can ever open. */}
|
|
375
|
+
{bundleImport.isError ? <MenuFailure>{i18n.t("tree.importFailed")}</MenuFailure> : null}
|
|
376
|
+
{importable ? bundleImport.inputs : null}
|
|
347
377
|
{/* ⚠️ A move has four refusals of its own and they are told apart by `moveErrorKey`, not by
|
|
348
378
|
`resourceErrorKey`: "that is not a folder" and "that would be a loop" have no equivalent
|
|
349
379
|
among the changes above, and one shared sentence would leave the reader guessing. */}
|
|
@@ -696,9 +726,7 @@ function SharePanel({ node, close }: { node: Node; close(): void }) {
|
|
|
696
726
|
className="size-4 rounded border outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
697
727
|
/>
|
|
698
728
|
<span>{i18n.t(`node.verb.${verb}`)}</span>
|
|
699
|
-
<span className="text-xs text-muted-foreground">
|
|
700
|
-
{i18n.t(verbHintKey(verb, node.kind))}
|
|
701
|
-
</span>
|
|
729
|
+
<span className="text-xs text-muted-foreground">{i18n.t(verbHintKey(verb))}</span>
|
|
702
730
|
</label>
|
|
703
731
|
))}
|
|
704
732
|
</fieldset>
|
|
@@ -5,8 +5,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
|
|
|
5
5
|
* An explanation behind an ⓘ, beside the thing it explains (#204, shared since #249).
|
|
6
6
|
*
|
|
7
7
|
* It exists for the sentence that answers "what IS this" — read once, then furniture. Not for the
|
|
8
|
-
* sentence you need WHILE you type:
|
|
9
|
-
*
|
|
8
|
+
* sentence you need WHILE you type: a reference read while writing must not be behind a hover, so
|
|
9
|
+
* something like a field's format ("five fields: minute, hour, …") stays a visible paragraph.
|
|
10
10
|
* That is the dividing line: *what is this* may hide, *how do I write this* may not.
|
|
11
11
|
*
|
|
12
12
|
* ⚠️ The hint is the trigger's accessible NAME, not only the tooltip's content: Radix describes the
|
|
@@ -11,47 +11,30 @@ import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu
|
|
|
11
11
|
* the rule could be forgotten. That is the whole point: the menu holds rename, move, share and
|
|
12
12
|
* archive, and a position one has to look for is a position one stops using.
|
|
13
13
|
*
|
|
14
|
-
* The
|
|
15
|
-
* state is. `title-meta` is a quiet word beside the name (a table's row count); `title-
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* before the menu for the same reason `children` do.
|
|
14
|
+
* The two slots are for what belongs to this thing but lives further down the tree, where its
|
|
15
|
+
* state is. `title-meta` is a quiet word beside the name (a table's row count); `title-actions` is a
|
|
16
|
+
* button in the group (saving a document, exporting a table). Both are filled through `ActionSlot`,
|
|
17
|
+
* and the one in the group sits before the menu for the same reason `children` do.
|
|
19
18
|
*/
|
|
20
19
|
export function TitleRow({
|
|
21
20
|
title,
|
|
22
21
|
description,
|
|
23
22
|
target,
|
|
24
|
-
leading,
|
|
25
|
-
divider = true,
|
|
26
23
|
children,
|
|
27
24
|
}: {
|
|
28
25
|
title: string;
|
|
29
26
|
description?: string | null;
|
|
30
27
|
target: ResourceTarget;
|
|
31
|
-
// ⚠️ A picture before the name, for the one kind that has a face: an agent's initials (#143). A
|
|
32
|
-
// prop rather than another slot because it must sit BEFORE the title, and a portal can only
|
|
33
|
-
// append — the same limitation that gave the shell two boxes instead of one list (#56).
|
|
34
|
-
leading?: React.ReactNode;
|
|
35
|
-
// Off for the one kind whose next line is its own tab bar: two rules a few pixels apart read as
|
|
36
|
-
// clutter, so the agent page keeps only the tab bar's (#204).
|
|
37
|
-
divider?: boolean;
|
|
38
28
|
children?: React.ReactNode;
|
|
39
29
|
}) {
|
|
40
30
|
return (
|
|
41
|
-
<div
|
|
42
|
-
className={`flex items-start justify-between gap-5 px-6 py-4${divider ? " border-b" : ""}`}
|
|
43
|
-
>
|
|
31
|
+
<div className="flex items-start justify-between gap-5 border-b px-6 py-4">
|
|
44
32
|
<div className="flex min-w-0 items-center gap-3">
|
|
45
|
-
{leading}
|
|
46
33
|
<div className="min-w-0">
|
|
47
34
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-3">
|
|
48
35
|
<h2 className="truncate text-lg font-semibold">{title}</h2>
|
|
49
36
|
<span data-slot="title-meta" className="text-sm text-muted-foreground" />
|
|
50
37
|
</div>
|
|
51
|
-
{/* ⚠️ `empty:hidden` is the whole rule for "no line rather than an empty one": nothing is
|
|
52
|
-
portalled in here for most kinds, and a slot that kept its margin would make every
|
|
53
|
-
head a few pixels taller for a line nobody wrote (#258). */}
|
|
54
|
-
<span data-slot="title-byline" className="mt-0.5 block min-w-0 empty:hidden" />
|
|
55
38
|
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
|
56
39
|
</div>
|
|
57
40
|
</div>
|
package/src/tools/tools.tsx
CHANGED
|
@@ -86,8 +86,6 @@ export function Tools() {
|
|
|
86
86
|
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
87
87
|
// The servers behind that same live list (D30). Its own query, and its own failure: a portal that
|
|
88
88
|
// answers tools but no directory still has a usable screen, it just has no boxes to put them in.
|
|
89
|
-
// The key is the one the agent profile already reads the directory under, so a visit to either
|
|
90
|
-
// screen serves the other from cache.
|
|
91
89
|
// ⚠️ No retry on this one, and that is a decision about the screen rather than about the request.
|
|
92
90
|
// The tools are held back until the directory has answered, so every further attempt is time
|
|
93
91
|
// somebody spends in front of the word "Loading" — and a retry does not even have to happen: a
|
package/vite.config.ts
CHANGED
|
@@ -12,10 +12,6 @@ export default defineConfig({
|
|
|
12
12
|
"/auth": { target: "http://localhost:8788", changeOrigin: true },
|
|
13
13
|
"/mcp": { target: "http://localhost:8788", changeOrigin: true },
|
|
14
14
|
"/.well-known": { target: "http://localhost:8788", changeOrigin: true },
|
|
15
|
-
// ⚠️ There is deliberately no `/agents` entry any more (#178). The browser reaches the agent
|
|
16
|
-
// runtime through intel, which forwards over its `AGENT` service binding — so in development
|
|
17
|
-
// the runtime still has to be running (`workers/agent`, port 8789, started separately from
|
|
18
|
-
// `bun run dev`), but Vite proxies nothing to it: wrangler's dev registry connects the two.
|
|
19
15
|
},
|
|
20
16
|
},
|
|
21
17
|
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The first letters of the words in a name, at most two.
|
|
5
|
-
*
|
|
6
|
-
* ⚠️ `Intl.Segmenter` rather than `slice(0, 1)`: an agent may be called "Émile" or "🛰 Radar", and
|
|
7
|
-
* a code unit is not a character. Slicing a surrogate pair in half puts a replacement glyph where
|
|
8
|
-
* the initial should be.
|
|
9
|
-
*/
|
|
10
|
-
export function initialsOf(title: string): string {
|
|
11
|
-
const words = title.trim().split(/\s+/).filter(Boolean).slice(0, 2);
|
|
12
|
-
const segmenter = new Intl.Segmenter();
|
|
13
|
-
return words
|
|
14
|
-
.map((word) => [...segmenter.segment(word)][0]?.segment ?? "")
|
|
15
|
-
.join("")
|
|
16
|
-
.toLocaleUpperCase();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Who the agent is, as a picture.
|
|
21
|
-
*
|
|
22
|
-
* ⚠️ Decorative on purpose: the name it abbreviates stands right beside it in the title line, so a
|
|
23
|
-
* screen reader that announced both would read the same name twice — once spelled out letter by
|
|
24
|
-
* letter. The picture is the redundant half here, not the text.
|
|
25
|
-
*/
|
|
26
|
-
export function AgentAvatar({ title }: { title: string }) {
|
|
27
|
-
return (
|
|
28
|
-
<Avatar aria-hidden="true" className="size-9">
|
|
29
|
-
<AvatarFallback className="bg-primary/10 text-sm font-semibold text-primary">
|
|
30
|
-
{initialsOf(title)}
|
|
31
|
-
</AvatarFallback>
|
|
32
|
-
</Avatar>
|
|
33
|
-
);
|
|
34
|
-
}
|