@anchrd/intel-ui 0.8.8 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +5 -2
- package/src/agent/agent-avatar/agent-avatar.tsx +34 -0
- package/src/agent/agent-calendar/agent-calendar.tsx +79 -0
- package/src/agent/agent-chat/agent-chat.tsx +116 -0
- package/src/agent/agent-cron/agent-cron.ts +132 -0
- package/src/agent/agent-definition/agent-definition.ts +64 -0
- package/src/agent/agent-entry-title/agent-entry-title.ts +29 -0
- package/src/agent/agent-log/agent-log.tsx +159 -0
- package/src/agent/agent-models/agent-models.ts +63 -0
- package/src/agent/agent-profile/agent-profile.tsx +547 -0
- package/src/agent/agent-state/agent-state.ts +48 -0
- package/src/agent/agent.tsx +361 -0
- package/src/app/app-sidebar/app-sidebar.tsx +2 -2
- package/src/app/app-tree/app-tree.tsx +151 -23
- package/src/app/app.tsx +2 -2
- package/src/app/header-search/header-search.tsx +16 -16
- package/src/app/tree-move/tree-move.tsx +10 -10
- package/src/archive/archive.tsx +8 -8
- package/src/components/ui/avatar.tsx +39 -0
- package/src/components/ui/select.tsx +163 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/data/agent-runtime/agent-runtime.ts +93 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +201 -120
- package/src/data/intel-data-provider/intel-data-provider.types.ts +112 -54
- package/src/entry-picker/entry-picker.tsx +53 -17
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +20 -20
- package/src/folder-contents/folder-contents.tsx +7 -7
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/hooks/use-capabilities.ts +22 -0
- package/src/i18n/en.json +147 -64
- package/src/kind-icon.ts +5 -2
- package/src/{knowledge-editor/knowledge-editor.tsx → node-editor/node-editor.tsx} +20 -20
- package/src/{knowledge-graph → node-graph}/graph-notice.tsx +1 -1
- package/src/{knowledge-graph/knowledge-graph.tsx → node-graph/node-graph.tsx} +4 -4
- package/src/{knowledge-table/knowledge-table.tsx → node-table/node-table.tsx} +14 -14
- package/src/{knowledge/knowledge.tsx → nodes/nodes.tsx} +47 -29
- package/src/resource-menu/resource-menu.tsx +105 -62
- package/src/router/router.tsx +5 -5
- package/src/title-row/title-row.tsx +20 -6
- package/vite.config.ts +4 -0
- /package/src/{knowledge-graph/knowledge-graph.ts → node-graph/node-graph.ts} +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import type { AgentDefinition, AgentScheduleTarget, Node } from "@anchrd/intel-contract";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { Pause, Play } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { AgentCalendar } from "@/agent/agent-calendar/agent-calendar.tsx";
|
|
6
|
+
import { AgentChat } from "@/agent/agent-chat/agent-chat.tsx";
|
|
7
|
+
import { nextRunAt, useAgentDefinition } from "@/agent/agent-definition/agent-definition.ts";
|
|
8
|
+
import { useEntryTitle } from "@/agent/agent-entry-title/agent-entry-title.ts";
|
|
9
|
+
import { AgentLog } from "@/agent/agent-log/agent-log.tsx";
|
|
10
|
+
import { AgentProfile } from "@/agent/agent-profile/agent-profile.tsx";
|
|
11
|
+
import {
|
|
12
|
+
agentAccessDenied,
|
|
13
|
+
agentRuntimeMissing,
|
|
14
|
+
agentStateKey,
|
|
15
|
+
useAgentState,
|
|
16
|
+
} from "@/agent/agent-state/agent-state.ts";
|
|
17
|
+
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuItem,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "@/components/ui/dropdown-menu";
|
|
24
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
25
|
+
import { useCapabilities } from "@/hooks/use-capabilities.ts";
|
|
26
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
27
|
+
|
|
28
|
+
const tabs = ["profile", "chat", "calendar", "log"] as const;
|
|
29
|
+
type Tab = (typeof tabs)[number];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The agent's page: the lower of the shell's two bars, carrying this one agent's four views.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ The name, the menu, the status line and the two head actions all live in the SAME title line —
|
|
35
|
+
* the one `TitleRow` draws for every open thing (#53). The status and the actions are portalled
|
|
36
|
+
* into its slots rather than drawn again here, because an agent with a header of its own would be
|
|
37
|
+
* the one kind of file whose rename sits somewhere else.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ One state query stands for the whole page, and it is also how the page learns whether this
|
|
40
|
+
* reader may drive this agent at all: `/session` carries identity and no capabilities on purpose, so
|
|
41
|
+
* a 403 from the runtime door is the only place the answer exists (#178). It is then rendered as an
|
|
42
|
+
* answer — a sentence in the two tabs that would have needed it — rather than as a red failure.
|
|
43
|
+
*/
|
|
44
|
+
export function AgentPanel({ node }: { node: Node }) {
|
|
45
|
+
const { i18n } = useIntelRouterContext();
|
|
46
|
+
const [tab, setTab] = useState<Tab>("profile");
|
|
47
|
+
const agent = useAgentDefinition(node.id);
|
|
48
|
+
const state = useAgentState(node.id);
|
|
49
|
+
const denied = agentAccessDenied(state.error);
|
|
50
|
+
const capabilities = useCapabilities();
|
|
51
|
+
|
|
52
|
+
// An installation without an agent runtime (#190): the node from an earlier deployment still
|
|
53
|
+
// opens, but it explains itself instead of rendering four views whose every call would end in a
|
|
54
|
+
// 503. `/capabilities` is the answer; the probe's own `agent_runtime_not_configured` is the belt
|
|
55
|
+
// to that brace. ⚠️ Only an explicit `false` takes the page away — while the answer is still
|
|
56
|
+
// loading the tabs stand, because tearing down a rendered page over a pending request would blank
|
|
57
|
+
// the screen for every agent on every load.
|
|
58
|
+
if (capabilities.data?.agentRuntime === false || agentRuntimeMissing(state.error)) {
|
|
59
|
+
return <NoRuntime />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<ActionSlot name="title-meta">
|
|
65
|
+
<AgentStatus agentId={node.id} definition={agent.definition} />
|
|
66
|
+
</ActionSlot>
|
|
67
|
+
<ActionSlot name="title-actions">
|
|
68
|
+
<AgentActions agentId={node.id} definition={agent.definition} />
|
|
69
|
+
</ActionSlot>
|
|
70
|
+
<Tabs
|
|
71
|
+
value={tab}
|
|
72
|
+
onValueChange={(value) => setTab(value as Tab)}
|
|
73
|
+
className="flex min-h-0 flex-1 flex-col gap-0"
|
|
74
|
+
>
|
|
75
|
+
{/* The file-specific bar. It stands under the title line and above the view it switches —
|
|
76
|
+
the order the flow screen's own switch already uses (#19). */}
|
|
77
|
+
<div className="border-b px-6 py-2">
|
|
78
|
+
<TabsList aria-label={i18n.t("agent.tabs")}>
|
|
79
|
+
{tabs.map((name) => (
|
|
80
|
+
<TabsTrigger key={name} value={name}>
|
|
81
|
+
{i18n.t(`agent.tab.${name}`)}
|
|
82
|
+
</TabsTrigger>
|
|
83
|
+
))}
|
|
84
|
+
</TabsList>
|
|
85
|
+
</div>
|
|
86
|
+
<TabsContent value="profile" className="flex min-h-0 flex-col">
|
|
87
|
+
<AgentProfile node={node} agent={agent} />
|
|
88
|
+
</TabsContent>
|
|
89
|
+
{/* ⚠️ Each of the three below is mounted only while its own tab is open. The chat is the
|
|
90
|
+
reason: it holds a live SSE connection, and one kept open behind three other views is a
|
|
91
|
+
stream nobody is reading. The other two follow so that a switch back re-reads rather
|
|
92
|
+
than showing a run list from ten minutes ago. */}
|
|
93
|
+
<TabsContent value="chat" className="flex min-h-0 flex-col">
|
|
94
|
+
{tab === "chat" ? denied ? <NotForYou /> : <AgentChat agentId={node.id} /> : null}
|
|
95
|
+
</TabsContent>
|
|
96
|
+
<TabsContent value="calendar" className="flex min-h-0 flex-col">
|
|
97
|
+
{tab === "calendar" ? (
|
|
98
|
+
<AgentCalendar definition={agent.definition} now={new Date()} />
|
|
99
|
+
) : null}
|
|
100
|
+
</TabsContent>
|
|
101
|
+
<TabsContent value="log" className="flex min-h-0 flex-col">
|
|
102
|
+
{tab === "log" ? denied ? <NotForYou /> : <AgentLog agentId={node.id} /> : null}
|
|
103
|
+
</TabsContent>
|
|
104
|
+
</Tabs>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What somebody without `agents/run` sees instead of the chat and the log.
|
|
111
|
+
*
|
|
112
|
+
* ⚠️ Muted text, not `role="alert"` and not the destructive colour. Nothing is broken and there is
|
|
113
|
+
* nothing to retry: they may read this agent, they may not drive it. A red box would send them to
|
|
114
|
+
* whoever runs the installation over a working system.
|
|
115
|
+
*/
|
|
116
|
+
function NotForYou() {
|
|
117
|
+
const { i18n } = useIntelRouterContext();
|
|
118
|
+
return (
|
|
119
|
+
<p className="m-auto max-w-md px-6 py-5 text-center text-sm text-muted-foreground">
|
|
120
|
+
{i18n.t("agent.notPermitted")}
|
|
121
|
+
</p>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* What an agent node from an earlier deployment shows where no runtime is configured (#190).
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ The same muted sentence as `NotForYou`, and for the same reason: nothing is broken and there
|
|
129
|
+
* is nothing to retry. The node and its definition are intact; the installation simply does not run
|
|
130
|
+
* agents, which is the operator's choice, not the reader's problem to debug.
|
|
131
|
+
*/
|
|
132
|
+
function NoRuntime() {
|
|
133
|
+
const { i18n } = useIntelRouterContext();
|
|
134
|
+
return (
|
|
135
|
+
<p className="m-auto max-w-md px-6 py-5 text-center text-sm text-muted-foreground">
|
|
136
|
+
{i18n.t("agent.noRuntime")}
|
|
137
|
+
</p>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Whether the agent is working, whether it is switched off, and when it next will be.
|
|
143
|
+
*
|
|
144
|
+
* ⚠️ "Running" is read from the runtime's own run list, and its absence is NOT read as "idle" — a
|
|
145
|
+
* runtime that cannot be reached says nothing about what the agent is doing, and a status line that
|
|
146
|
+
* quietly reported "idle" for an unreachable runtime would be wrong exactly when it matters.
|
|
147
|
+
*
|
|
148
|
+
* ⚠️ "Next run" is derived from the cron expressions rather than asked. The runtime arms one alarm
|
|
149
|
+
* and publishes neither it nor a schedule, so this is the same reading `AgentCalendar` does — and
|
|
150
|
+
* while the agent is paused it is not shown at all, because a paused agent will not run then.
|
|
151
|
+
*/
|
|
152
|
+
export function AgentStatus({
|
|
153
|
+
agentId,
|
|
154
|
+
definition,
|
|
155
|
+
}: {
|
|
156
|
+
agentId: string;
|
|
157
|
+
definition: AgentDefinition | null;
|
|
158
|
+
}) {
|
|
159
|
+
const { data, i18n } = useIntelRouterContext();
|
|
160
|
+
const state = useAgentState(agentId);
|
|
161
|
+
const runs = useQuery({
|
|
162
|
+
queryKey: ["agent-runs", agentId],
|
|
163
|
+
queryFn: () => data.listAgentRuns(agentId),
|
|
164
|
+
// A status line is not worth a red screen: the runtime being unreachable is reported by the
|
|
165
|
+
// Log tab, which is the view that is actually about runs.
|
|
166
|
+
retry: false,
|
|
167
|
+
});
|
|
168
|
+
const running = runs.data?.items.some((run) => run.status === "running") ?? false;
|
|
169
|
+
const paused = state.data?.paused ?? false;
|
|
170
|
+
const next = nextRunAt(definition, new Date());
|
|
171
|
+
const parts = [
|
|
172
|
+
runs.isError
|
|
173
|
+
? i18n.t("agent.statusUnknown")
|
|
174
|
+
: paused
|
|
175
|
+
? i18n.t("agent.statusPaused")
|
|
176
|
+
: running
|
|
177
|
+
? i18n.t("agent.statusRunning")
|
|
178
|
+
: i18n.t("agent.statusIdle"),
|
|
179
|
+
paused
|
|
180
|
+
? i18n.t("agent.pausedNoRuns")
|
|
181
|
+
: next
|
|
182
|
+
? i18n.t("agent.nextRun", {
|
|
183
|
+
when: new Intl.DateTimeFormat(i18n.locale, {
|
|
184
|
+
dateStyle: "medium",
|
|
185
|
+
timeStyle: "short",
|
|
186
|
+
timeZone: "UTC",
|
|
187
|
+
}).format(next),
|
|
188
|
+
})
|
|
189
|
+
: i18n.t("agent.noNextRun"),
|
|
190
|
+
];
|
|
191
|
+
return <span>{parts.join(" · ")}</span>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Pause, and run now.
|
|
196
|
+
*
|
|
197
|
+
* ⚠️ Both are shown even where they cannot be used, against this repo's usual rule that an
|
|
198
|
+
* inapplicable action is absent. The rule is about actions that do not APPLY to a kind — a folder
|
|
199
|
+
* has no retrieval mode. These two apply to every agent, and a head that quietly dropped them would
|
|
200
|
+
* hide from the reader that the agent can be stopped at all. Disabled always carries its reason on
|
|
201
|
+
* the screen, pointed at by `aria-describedby`, so the keyboard hears it too.
|
|
202
|
+
*
|
|
203
|
+
* ⚠️ "Run now" fires a schedule the agent already has; it does not invent one. A run acts with the
|
|
204
|
+
* AGENT's Gate token, so a free choice of target here would let anybody who may drive the agent read
|
|
205
|
+
* with its grants instead of their own — the runtime refuses an undeclared target for that reason,
|
|
206
|
+
* and this menu offers only what it would accept.
|
|
207
|
+
*/
|
|
208
|
+
export function AgentActions({
|
|
209
|
+
agentId,
|
|
210
|
+
definition,
|
|
211
|
+
}: {
|
|
212
|
+
agentId: string;
|
|
213
|
+
definition: AgentDefinition | null;
|
|
214
|
+
}) {
|
|
215
|
+
const { data, i18n } = useIntelRouterContext();
|
|
216
|
+
const queryClient = useQueryClient();
|
|
217
|
+
const state = useAgentState(agentId);
|
|
218
|
+
const denied = agentAccessDenied(state.error);
|
|
219
|
+
const paused = state.data?.paused ?? false;
|
|
220
|
+
|
|
221
|
+
const toggle = useMutation({
|
|
222
|
+
mutationFn: async () =>
|
|
223
|
+
paused ? await data.resumeAgent(agentId) : await data.pauseAgent(agentId),
|
|
224
|
+
// The answer is the new state, so the cache is set from it rather than refetched: a round trip
|
|
225
|
+
// would leave a window in which the button still offers what was just done.
|
|
226
|
+
onSuccess: (next) => queryClient.setQueryData(agentStateKey(agentId), next),
|
|
227
|
+
});
|
|
228
|
+
const runNow = useMutation({
|
|
229
|
+
mutationFn: async (target: AgentScheduleTarget) => await data.runAgentNow(agentId, target),
|
|
230
|
+
// The run is worked off the runtime's alarm, so it appears in the list rather than in this
|
|
231
|
+
// answer. Invalidating is what puts it on the Log tab and in the status line.
|
|
232
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["agent-runs", agentId] }),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const targets = definition?.schedules.map((schedule) => schedule.target) ?? [];
|
|
236
|
+
const reason = denied
|
|
237
|
+
? i18n.t("agent.notPermitted")
|
|
238
|
+
: state.isError
|
|
239
|
+
? i18n.t("agent.statusUnknown")
|
|
240
|
+
: null;
|
|
241
|
+
const runReason =
|
|
242
|
+
reason ??
|
|
243
|
+
(paused
|
|
244
|
+
? i18n.t("agent.runNowPaused")
|
|
245
|
+
: targets.length === 0
|
|
246
|
+
? i18n.t("agent.runNowNoTarget")
|
|
247
|
+
: null);
|
|
248
|
+
const failure = toggle.error ?? runNow.error;
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<span className="flex items-center gap-2">
|
|
252
|
+
{/* ⚠️ `sr-only` rather than `hidden` where the bar is narrow. A disabled control whose reason
|
|
253
|
+
is `display: none` has no reason at all for a screen reader — the element it points at is
|
|
254
|
+
not in the accessibility tree. */}
|
|
255
|
+
{runReason ? (
|
|
256
|
+
<span
|
|
257
|
+
id="agent-actions-reason"
|
|
258
|
+
className="text-xs text-muted-foreground max-sm:sr-only sm:inline"
|
|
259
|
+
>
|
|
260
|
+
{runReason}
|
|
261
|
+
</span>
|
|
262
|
+
) : null}
|
|
263
|
+
{failure ? (
|
|
264
|
+
<span role="alert" className="text-xs text-destructive max-sm:sr-only sm:inline">
|
|
265
|
+
{i18n.t("agent.actionFailed")}
|
|
266
|
+
</span>
|
|
267
|
+
) : null}
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => toggle.mutate()}
|
|
271
|
+
disabled={reason !== null || state.isPending || toggle.isPending}
|
|
272
|
+
aria-busy={toggle.isPending}
|
|
273
|
+
{...(reason ? { "aria-describedby": "agent-actions-reason" } : {})}
|
|
274
|
+
className="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
275
|
+
>
|
|
276
|
+
{paused ? (
|
|
277
|
+
<Play aria-hidden="true" className="size-4" />
|
|
278
|
+
) : (
|
|
279
|
+
<Pause aria-hidden="true" className="size-4" />
|
|
280
|
+
)}
|
|
281
|
+
{i18n.t(paused ? "agent.resume" : "agent.pause")}
|
|
282
|
+
</button>
|
|
283
|
+
<RunNow
|
|
284
|
+
targets={targets}
|
|
285
|
+
disabled={runReason !== null || runNow.isPending}
|
|
286
|
+
busy={runNow.isPending}
|
|
287
|
+
describedBy={runReason ? "agent-actions-reason" : undefined}
|
|
288
|
+
start={(target) => runNow.mutate(target)}
|
|
289
|
+
/>
|
|
290
|
+
</span>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* ⚠️ One schedule runs straight away; several open a menu. A button that silently picked the first
|
|
296
|
+
* of three targets would run the wrong thing at a moment somebody chose deliberately, and a menu in
|
|
297
|
+
* front of a single obvious target is a click that answers nothing.
|
|
298
|
+
*/
|
|
299
|
+
function RunNow({
|
|
300
|
+
targets,
|
|
301
|
+
disabled,
|
|
302
|
+
busy,
|
|
303
|
+
describedBy,
|
|
304
|
+
start,
|
|
305
|
+
}: {
|
|
306
|
+
targets: AgentScheduleTarget[];
|
|
307
|
+
disabled: boolean;
|
|
308
|
+
busy: boolean;
|
|
309
|
+
describedBy: string | undefined;
|
|
310
|
+
start(target: AgentScheduleTarget): void;
|
|
311
|
+
}) {
|
|
312
|
+
const { i18n } = useIntelRouterContext();
|
|
313
|
+
const only = targets.length === 1 ? targets[0] : undefined;
|
|
314
|
+
const className =
|
|
315
|
+
"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50";
|
|
316
|
+
|
|
317
|
+
if (targets.length > 1) {
|
|
318
|
+
return (
|
|
319
|
+
<DropdownMenu>
|
|
320
|
+
<DropdownMenuTrigger asChild>
|
|
321
|
+
<button
|
|
322
|
+
type="button"
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
aria-busy={busy}
|
|
325
|
+
{...(describedBy ? { "aria-describedby": describedBy } : {})}
|
|
326
|
+
className={className}
|
|
327
|
+
>
|
|
328
|
+
<Play aria-hidden="true" className="size-4" />
|
|
329
|
+
{i18n.t("agent.runNow")}
|
|
330
|
+
</button>
|
|
331
|
+
</DropdownMenuTrigger>
|
|
332
|
+
<DropdownMenuContent align="end">
|
|
333
|
+
{targets.map((target) => (
|
|
334
|
+
<DropdownMenuItem key={`${target.kind}:${target.id}`} onSelect={() => start(target)}>
|
|
335
|
+
<TargetLabel target={target} />
|
|
336
|
+
</DropdownMenuItem>
|
|
337
|
+
))}
|
|
338
|
+
</DropdownMenuContent>
|
|
339
|
+
</DropdownMenu>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={() => only && start(only)}
|
|
347
|
+
disabled={disabled || !only}
|
|
348
|
+
aria-busy={busy}
|
|
349
|
+
{...(describedBy ? { "aria-describedby": describedBy } : {})}
|
|
350
|
+
className={className}
|
|
351
|
+
>
|
|
352
|
+
<Play aria-hidden="true" className="size-4" />
|
|
353
|
+
{i18n.t("agent.runNow")}
|
|
354
|
+
</button>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function TargetLabel({ target }: { target: AgentScheduleTarget }) {
|
|
359
|
+
const { title } = useEntryTitle(target.id, target.kind === "flow");
|
|
360
|
+
return <span className="truncate">{title}</span>;
|
|
361
|
+
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
17
17
|
|
|
18
18
|
// One tree is the whole navigation: documents and flows side by side in the folder they share, with
|
|
19
|
-
// no
|
|
19
|
+
// no Intelligence or Flows entry above them (ADR-0004). Tools stay below, next to the user, because
|
|
20
20
|
// they come live from the portal and have no tree.
|
|
21
21
|
export function AppSidebar({
|
|
22
22
|
width,
|
|
@@ -45,7 +45,7 @@ export function AppSidebar({
|
|
|
45
45
|
<SidebarMenu>
|
|
46
46
|
<SidebarMenuItem>
|
|
47
47
|
<SidebarMenuButton size="lg" asChild>
|
|
48
|
-
<Link to="/
|
|
48
|
+
<Link to="/nodes">
|
|
49
49
|
<AppLogo className="size-8" />
|
|
50
50
|
<span className="font-semibold">{i18n.t("app.name")}</span>
|
|
51
51
|
</Link>
|