@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.
Files changed (63) hide show
  1. package/package.json +2 -9
  2. package/src/app/app-tree/app-tree.tsx +59 -147
  3. package/src/app/settings-dialog/settings-dialog.tsx +10 -32
  4. package/src/data/intel-data-provider/intel-data-provider.ts +0 -163
  5. package/src/data/intel-data-provider/intel-data-provider.types.ts +0 -100
  6. package/src/entry-picker/entry-picker.tsx +6 -6
  7. package/src/i18n/de.json +6 -217
  8. package/src/i18n/en.json +6 -217
  9. package/src/i18n/es.json +6 -217
  10. package/src/import-bundle/import-bundle.tsx +152 -0
  11. package/src/kind-icon.ts +1 -15
  12. package/src/main.tsx +7 -10
  13. package/src/node-table/node-table.tsx +2 -2
  14. package/src/nodes/nodes.tsx +0 -34
  15. package/src/resource-menu/resource-menu.tsx +42 -14
  16. package/src/section-hint/section-hint.tsx +2 -2
  17. package/src/title-row/title-row.tsx +5 -22
  18. package/src/tools/tools.tsx +0 -2
  19. package/vite.config.ts +0 -4
  20. package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
  21. package/src/agent/agent-calendar/agent-calendar.tsx +0 -160
  22. package/src/agent/agent-chat/agent-chat.tsx +0 -118
  23. package/src/agent/agent-costs/agent-costs.ts +0 -95
  24. package/src/agent/agent-cron/agent-cron.ts +0 -68
  25. package/src/agent/agent-definition/agent-definition.ts +0 -96
  26. package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
  27. package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
  28. package/src/agent/agent-log/agent-log.tsx +0 -308
  29. package/src/agent/agent-models/agent-models.ts +0 -166
  30. package/src/agent/agent-models/agent-models.types.ts +0 -24
  31. package/src/agent/agent-profile/agent-profile.tsx +0 -1006
  32. package/src/agent/agent-state/agent-state.ts +0 -85
  33. package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
  34. package/src/agent/agent.tsx +0 -507
  35. package/src/board/board-calendar/board-calendar.tsx +0 -89
  36. package/src/board/board-card/board-card.tsx +0 -106
  37. package/src/board/board-data/board-data.ts +0 -241
  38. package/src/board/board-data/board-data.types.ts +0 -63
  39. package/src/board/board-detail/board-detail.tsx +0 -629
  40. package/src/board/board-gantt/board-gantt.ts +0 -545
  41. package/src/board/board-gantt/board-gantt.tsx +0 -286
  42. package/src/board/board-graph/board-graph.ts +0 -174
  43. package/src/board/board-graph/board-graph.tsx +0 -168
  44. package/src/board/board-items/board-items.ts +0 -183
  45. package/src/board/board-kanban/board-kanban.ts +0 -137
  46. package/src/board/board-kanban/board-kanban.tsx +0 -257
  47. package/src/board/board-status/board-status.ts +0 -59
  48. package/src/board/board-statuses/board-statuses.ts +0 -63
  49. package/src/board/board-statuses/board-statuses.tsx +0 -228
  50. package/src/board/board-table/board-table.ts +0 -33
  51. package/src/board/board-table/board-table.tsx +0 -413
  52. package/src/board/board-views/board-views.tsx +0 -68
  53. package/src/board/board-views/board-views.types.ts +0 -29
  54. package/src/board/board.tsx +0 -251
  55. package/src/components/ui/item-calendar.tsx +0 -181
  56. package/src/components/ui/item-gantt.tsx +0 -463
  57. package/src/components/ui/kanban.tsx +0 -282
  58. package/src/data/agent-runtime/agent-runtime.ts +0 -131
  59. package/src/hooks/use-capabilities.ts +0 -22
  60. package/src/hooks/use-model-catalog.ts +0 -26
  61. package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
  62. package/src/timezone/timezone-context.tsx +0 -65
  63. 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
- <TimezoneProvider store={window.localStorage}>
59
- {refused ? (
60
- <SignInRefused />
61
- ) : (
62
- <QueryClientProvider client={queryClient}>
63
- <RouterProvider router={router} />
64
- </QueryClientProvider>
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 agent fills it without a person
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
@@ -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, for THIS kind of thing.
89
+ * Which sentence explains a verb.
85
90
  *
86
- * ⚠️ "Run" does not mean the same thing twice. On a folder it grants starting the flows filed in
87
- * it; on an agent it grants using the agent through its chat, its schedules and its MCP address.
88
- * Until #143 there was one sentence for both, and on an agent it named flows that have nothing to
89
- * do with the grant being made. A key per kind, falling back to the general one, so a kind that
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, kind: NodeKind): string {
93
- return verb === "execute" && kind === "agent"
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: `agent.cronHint` ("five fields: minute, hour, …") deliberately
9
- * stays a visible paragraph, because a reference you read while writing must not be behind a hover.
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 three 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-byline` is a
16
- * quiet line under it (who made an agent, #258); `title-actions` is a button in the group (saving a
17
- * document, exporting a table). All are filled through `ActionSlot`, and the two in the group sit
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>
@@ -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
- }