@anchrd/intel-ui 0.18.0 → 0.20.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 +68 -147
- package/src/app/header-search/header-search.tsx +2 -1
- package/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/src/app/settings-dialog/settings-dialog.tsx +10 -32
- package/src/app/user-footer/user-footer.tsx +19 -0
- package/src/archive/archive.tsx +2 -1
- package/src/data/intel-data-provider/intel-data-provider.ts +42 -191
- package/src/data/intel-data-provider/intel-data-provider.types.ts +36 -120
- package/src/document-link/document-link.tsx +1 -1
- package/src/entry-picker/entry-picker.tsx +7 -7
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +3 -2
- package/src/flows/node-icon/node-icon.ts +1 -1
- package/src/flows/node-palette/node-palette.tsx +1 -1
- package/src/flows/node-palette/node-palette.types.ts +1 -1
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/i18n/de.json +14 -217
- package/src/i18n/en.json +14 -217
- package/src/i18n/es.json +14 -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-editor/node-editor.tsx +1 -1
- package/src/node-graph/graph-notice.tsx +1 -1
- package/src/node-graph/node-graph.ts +1 -1
- package/src/node-graph/node-graph.tsx +1 -1
- package/src/node-table/node-table.tsx +3 -3
- package/src/nodes/nodes.tsx +1 -35
- package/src/resource-menu/resource-menu.tsx +45 -21
- package/src/section-hint/section-hint.tsx +2 -2
- package/src/title-row/title-row.tsx +5 -22
- package/src/tools/tools.tsx +1 -3
- 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
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
|
|
2
|
-
import type { AgentState } from "@/data/agent-runtime/agent-runtime.ts";
|
|
3
|
-
import { IntelRequestError } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
4
|
-
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
5
|
-
|
|
6
|
-
export const agentStateKey = (agentId: string) => ["agent-state", agentId] as const;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Whether this agent is switched off, read from the runtime through intel.
|
|
10
|
-
*
|
|
11
|
-
* ⚠️ One query for the whole page, shared by the status line, the two head actions and the tabs
|
|
12
|
-
* that have to say "not for you". It is also the page's PROBE: the answer to "may this reader drive
|
|
13
|
-
* this agent" is only knowable by asking, because `/session` deliberately carries identity and no
|
|
14
|
-
* capabilities (`SessionUser` — "no capabilities, no token"). So a 403 here is not a failure to
|
|
15
|
-
* report but a fact to render.
|
|
16
|
-
*/
|
|
17
|
-
export function useAgentState(agentId: string): UseQueryResult<AgentState> {
|
|
18
|
-
const { data } = useIntelRouterContext();
|
|
19
|
-
return useQuery({
|
|
20
|
-
queryKey: agentStateKey(agentId),
|
|
21
|
-
queryFn: () => data.getAgentState(agentId),
|
|
22
|
-
// A refusal is an answer, and retrying it three times only delays the sentence that explains it.
|
|
23
|
-
retry: false,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The refusal that is about permission rather than about something being broken.
|
|
29
|
-
*
|
|
30
|
-
* ⚠️ 403 and nothing else. A 401 has already sent the page to the sign-in, and a 503 means this
|
|
31
|
-
* installation runs no agent runtime — reading either as "you may not" would tell somebody they
|
|
32
|
-
* lack a permission they actually have.
|
|
33
|
-
*/
|
|
34
|
-
export function agentAccessDenied(error: unknown): boolean {
|
|
35
|
-
return error instanceof IntelRequestError && error.status === 403;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The refusal that says this installation runs no agent runtime at all (#190).
|
|
40
|
-
*
|
|
41
|
-
* ⚠️ The code, not the 503: a runtime that exists but is down may answer 503 too, and reading that
|
|
42
|
-
* as "this deployment has no agents" would explain a permanent absence to somebody who is looking
|
|
43
|
-
* at an outage. Normally `/capabilities` has already said so and the page never asks — this is the
|
|
44
|
-
* belt to that brace, for the probe that reached the door anyway.
|
|
45
|
-
*/
|
|
46
|
-
export function agentRuntimeMissing(error: unknown): boolean {
|
|
47
|
-
return error instanceof IntelRequestError && error.code === "agent_runtime_not_configured";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The failure that has a repair path, and a name for it (D29, #207, and the short half of #200).
|
|
52
|
-
*
|
|
53
|
-
* ⚠️ The code, not the 500. An agent whose Durable Object holds no key starts no run at all — the
|
|
54
|
-
* runtime refuses before Gate is asked — and until #207 that arrived on this page as
|
|
55
|
-
* `That did not work. Try again.` next to `Run now`, which is advice that cannot work: trying again
|
|
56
|
-
* produces the same refusal forever. Naming it is what lets the page send the reader to the one
|
|
57
|
-
* thing that fixes it, which is replacing the key in the agent's profile.
|
|
58
|
-
*
|
|
59
|
-
* ⚠️ `agent_key_unreadable` counts as the same case on purpose. Its cause is different — the
|
|
60
|
-
* deployment's master secret was replaced — but the reader's move is identical, and a second
|
|
61
|
-
* sentence saying the same thing differently would only be a second thing to keep in step.
|
|
62
|
-
*/
|
|
63
|
-
/**
|
|
64
|
-
* The refusal in the words the refusal itself used (#201).
|
|
65
|
-
*
|
|
66
|
-
* ⚠️ Only for an answer that carried a problem document — `code` is what proves it did. Everything
|
|
67
|
-
* else that reaches here is a timeout, a proxy page or a parse failure, whose `message` is
|
|
68
|
-
* `Intel responded with 502`: true, and no help to anybody. Those keep the generic sentence.
|
|
69
|
-
*
|
|
70
|
-
* The reason has to reach the screen because `Resume` can fail for reasons only the runtime knows —
|
|
71
|
-
* "none of this agent's schedules has a next fire time" is one, and it is not something a reader
|
|
72
|
-
* can guess from `That did not work. Try again.` The runtime words its problems to be read (see
|
|
73
|
-
* `packages/agent`'s guard on model-facing errors), and the log tab already shows a run's failure
|
|
74
|
-
* verbatim for the same reason.
|
|
75
|
-
*/
|
|
76
|
-
export function agentProblemDetail(error: unknown): string | null {
|
|
77
|
-
return error instanceof IntelRequestError && error.code !== null ? error.message : null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function agentKeyMissing(error: unknown): boolean {
|
|
81
|
-
return (
|
|
82
|
-
error instanceof IntelRequestError &&
|
|
83
|
-
(error.code === "agent_key_missing" || error.code === "agent_key_unreadable")
|
|
84
|
-
);
|
|
85
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { AgentRun } from "@/data/agent-runtime/agent-runtime.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The five things an agent's head can have to say. There is deliberately no "idle": the absence of
|
|
5
|
-
* a state is not a state, and writing it out at the most prominent place on the page was what #253
|
|
6
|
-
* removed.
|
|
7
|
-
*/
|
|
8
|
-
export type AgentStatusKind = "unknown" | "paused" | "running" | "failed" | "waiting";
|
|
9
|
-
|
|
10
|
-
export interface AgentStatusReading {
|
|
11
|
-
kind: AgentStatusKind;
|
|
12
|
-
/** When the newest finished run ended — `null` while the agent has never finished one. */
|
|
13
|
-
lastRunAt: string | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface AgentStatusInput {
|
|
17
|
-
/** The run list could not be read. NOT the same as "no runs". */
|
|
18
|
-
runsUnreadable: boolean;
|
|
19
|
-
/** Newest first, the way the runtime answers. */
|
|
20
|
-
runs: AgentRun[];
|
|
21
|
-
paused: boolean;
|
|
22
|
-
/** Whether any schedule will fire — derived from the cron expressions, never asked. */
|
|
23
|
-
scheduled: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* What the head says about this agent, or `null` when it has nothing to say (#253).
|
|
28
|
-
*
|
|
29
|
-
* ⚠️ `null` is the ordinary case and the whole point of the ticket: an agent with no schedule, no
|
|
30
|
-
* run in flight and no failure behind it is not "Idle · no scheduled run", it is a name. Two
|
|
31
|
-
* sentences spelling out the same absence stood at the most prominent place on the page.
|
|
32
|
-
*
|
|
33
|
-
* ⚠️ It is NOT simply "has a schedule". An agent somebody drives from the chat can be running, can
|
|
34
|
-
* be switched off, and can have failed its last run without ever carrying a schedule — hiding any
|
|
35
|
-
* of those would trade two useless sentences for one missing fact. What disappears is the absence,
|
|
36
|
-
* not the presence.
|
|
37
|
-
*
|
|
38
|
-
* ⚠️ An unreadable run list outranks everything and is its own state. A runtime that cannot be
|
|
39
|
-
* reached says nothing about what the agent is doing, and reporting "waiting" for it would be wrong
|
|
40
|
-
* exactly when something is wrong.
|
|
41
|
-
*
|
|
42
|
-
* ⚠️ Paused outranks running, the way the status line has always read it: a run still finishing
|
|
43
|
-
* while the agent is switched off does not make the agent switched on, and the reader's next
|
|
44
|
-
* question is why nothing starts afterwards.
|
|
45
|
-
*/
|
|
46
|
-
export function agentStatus(input: AgentStatusInput): AgentStatusReading | null {
|
|
47
|
-
const finished = input.runs.find((run) => run.status !== "running") ?? null;
|
|
48
|
-
const lastRunAt = finished?.finishedAt ?? finished?.startedAt ?? null;
|
|
49
|
-
|
|
50
|
-
if (input.runsUnreadable) return { kind: "unknown", lastRunAt: null };
|
|
51
|
-
if (input.paused) return { kind: "paused", lastRunAt };
|
|
52
|
-
if (input.runs.some((run) => run.status === "running")) return { kind: "running", lastRunAt };
|
|
53
|
-
if (finished?.status === "failed") return { kind: "failed", lastRunAt };
|
|
54
|
-
if (input.scheduled) return { kind: "waiting", lastRunAt };
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* How each state is drawn.
|
|
60
|
-
*
|
|
61
|
-
* ⚠️ Shape carries what colour cannot. This theme is monochrome apart from `destructive` (see
|
|
62
|
-
* `styles.css`), so five states cannot be five hues without inventing colours the customer's own
|
|
63
|
-
* theme would then fight — and colour alone is no information for a reader who cannot separate two
|
|
64
|
-
* greys. Filled, hollow, dashed and moving are told apart without any hue at all, and the state is
|
|
65
|
-
* in the trigger's accessible name regardless.
|
|
66
|
-
*/
|
|
67
|
-
export const statusDotClass: Record<AgentStatusKind, string> = {
|
|
68
|
-
unknown: "border border-dashed border-muted-foreground",
|
|
69
|
-
paused: "bg-muted-foreground",
|
|
70
|
-
running: "bg-primary motion-safe:animate-pulse",
|
|
71
|
-
failed: "bg-destructive",
|
|
72
|
-
waiting: "border border-muted-foreground",
|
|
73
|
-
};
|
package/src/agent/agent.tsx
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
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
|
-
agentKeyMissing,
|
|
14
|
-
agentProblemDetail,
|
|
15
|
-
agentRuntimeMissing,
|
|
16
|
-
agentStateKey,
|
|
17
|
-
useAgentState,
|
|
18
|
-
} from "@/agent/agent-state/agent-state.ts";
|
|
19
|
-
import { agentStatus, statusDotClass } from "@/agent/agent-status-dot/agent-status-dot.ts";
|
|
20
|
-
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
21
|
-
import {
|
|
22
|
-
DropdownMenu,
|
|
23
|
-
DropdownMenuContent,
|
|
24
|
-
DropdownMenuItem,
|
|
25
|
-
DropdownMenuTrigger,
|
|
26
|
-
} from "@/components/ui/dropdown-menu";
|
|
27
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
28
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
29
|
-
import { useCapabilities } from "@/hooks/use-capabilities.ts";
|
|
30
|
-
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
31
|
-
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
32
|
-
import { useUserName } from "@/user-name/user-name.ts";
|
|
33
|
-
|
|
34
|
-
const tabs = ["profile", "chat", "calendar", "log"] as const;
|
|
35
|
-
type Tab = (typeof tabs)[number];
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The agent's page: the lower of the shell's two bars, carrying this one agent's four views.
|
|
39
|
-
*
|
|
40
|
-
* ⚠️ The name, the menu, the status line and the two head actions all live in the SAME title line —
|
|
41
|
-
* the one `TitleRow` draws for every open thing (#53). The status and the actions are portalled
|
|
42
|
-
* into its slots rather than drawn again here, because an agent with a header of its own would be
|
|
43
|
-
* the one kind of file whose rename sits somewhere else.
|
|
44
|
-
*
|
|
45
|
-
* ⚠️ One state query stands for the whole page, and it is also how the page learns whether this
|
|
46
|
-
* reader may drive this agent at all: `/session` carries identity and no capabilities on purpose, so
|
|
47
|
-
* a 403 from the runtime door is the only place the answer exists (#178). It is then rendered as an
|
|
48
|
-
* answer — a sentence in the two tabs that would have needed it — rather than as a red failure.
|
|
49
|
-
*/
|
|
50
|
-
export function AgentPanel({ node }: { node: Node }) {
|
|
51
|
-
const i18n = useI18n();
|
|
52
|
-
const [tab, setTab] = useState<Tab>("profile");
|
|
53
|
-
const agent = useAgentDefinition(node.id);
|
|
54
|
-
const state = useAgentState(node.id);
|
|
55
|
-
const denied = agentAccessDenied(state.error);
|
|
56
|
-
const capabilities = useCapabilities();
|
|
57
|
-
|
|
58
|
-
// An installation without an agent runtime (#190): the node from an earlier deployment still
|
|
59
|
-
// opens, but it explains itself instead of rendering four views whose every call would end in a
|
|
60
|
-
// 503. `/capabilities` is the answer; the probe's own `agent_runtime_not_configured` is the belt
|
|
61
|
-
// to that brace. ⚠️ Only an explicit `false` takes the page away — while the answer is still
|
|
62
|
-
// loading the tabs stand, because tearing down a rendered page over a pending request would blank
|
|
63
|
-
// the screen for every agent on every load.
|
|
64
|
-
if (capabilities.data?.agentRuntime === false || agentRuntimeMissing(state.error)) {
|
|
65
|
-
return <NoRuntime />;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<>
|
|
70
|
-
<ActionSlot name="title-meta">
|
|
71
|
-
<AgentStatus agentId={node.id} definition={agent.definition} />
|
|
72
|
-
</ActionSlot>
|
|
73
|
-
<ActionSlot name="title-byline">
|
|
74
|
-
<AgentByline node={node} />
|
|
75
|
-
</ActionSlot>
|
|
76
|
-
<ActionSlot name="title-actions">
|
|
77
|
-
<AgentActions agentId={node.id} definition={agent.definition} />
|
|
78
|
-
</ActionSlot>
|
|
79
|
-
<Tabs
|
|
80
|
-
value={tab}
|
|
81
|
-
onValueChange={(value) => setTab(value as Tab)}
|
|
82
|
-
className="flex min-h-0 flex-1 flex-col gap-0"
|
|
83
|
-
>
|
|
84
|
-
{/* The file-specific bar. It stands under the title line and above the view it switches —
|
|
85
|
-
the order the flow screen's own switch already uses (#19). */}
|
|
86
|
-
<div className="border-b px-6 py-2">
|
|
87
|
-
<TabsList aria-label={i18n.t("agent.tabs")}>
|
|
88
|
-
{tabs.map((name) => (
|
|
89
|
-
<TabsTrigger key={name} value={name}>
|
|
90
|
-
{i18n.t(`agent.tab.${name}`)}
|
|
91
|
-
</TabsTrigger>
|
|
92
|
-
))}
|
|
93
|
-
</TabsList>
|
|
94
|
-
</div>
|
|
95
|
-
<TabsContent value="profile" className="flex min-h-0 flex-col">
|
|
96
|
-
<AgentProfile node={node} agent={agent} />
|
|
97
|
-
</TabsContent>
|
|
98
|
-
{/* ⚠️ Each of the three below is mounted only while its own tab is open. The chat is the
|
|
99
|
-
reason: it holds a live SSE connection, and one kept open behind three other views is a
|
|
100
|
-
stream nobody is reading. The other two follow so that a switch back re-reads rather
|
|
101
|
-
than showing a run list from ten minutes ago. */}
|
|
102
|
-
<TabsContent value="chat" className="flex min-h-0 flex-col">
|
|
103
|
-
{tab === "chat" ? denied ? <NotForYou /> : <AgentChat agentId={node.id} /> : null}
|
|
104
|
-
</TabsContent>
|
|
105
|
-
<TabsContent value="calendar" className="flex min-h-0 flex-col">
|
|
106
|
-
{tab === "calendar" ? (
|
|
107
|
-
<AgentCalendar
|
|
108
|
-
definition={agent.definition}
|
|
109
|
-
now={new Date()}
|
|
110
|
-
// ⚠️ The same `state` the header reads, so the two cannot disagree about one agent —
|
|
111
|
-
// and the pause button writes its answer straight into this query key, so the note
|
|
112
|
-
// goes the moment somebody resumes, without this tab refetching (#203).
|
|
113
|
-
paused={state.data?.paused ?? false}
|
|
114
|
-
/>
|
|
115
|
-
) : null}
|
|
116
|
-
</TabsContent>
|
|
117
|
-
<TabsContent value="log" className="flex min-h-0 flex-col">
|
|
118
|
-
{tab === "log" ? denied ? <NotForYou /> : <AgentLog agentId={node.id} /> : null}
|
|
119
|
-
</TabsContent>
|
|
120
|
-
</Tabs>
|
|
121
|
-
</>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Who made this agent — the first question anybody asks about one that works in folders with rights
|
|
127
|
-
* of its own (#258).
|
|
128
|
-
*
|
|
129
|
-
* ⚠️ Provenance and nothing else. An agent does NOT run with its creator's rights: it has a Gate
|
|
130
|
-
* Application of its own (D29), the assertion's subject is the agent, and Intel's grants on the
|
|
131
|
-
* tree decide what a run may read and write. Only the TOOLS run on a human's connection, and that
|
|
132
|
-
* is `delegatedBy` — a different field, in a different place on this page, on purpose. So this line
|
|
133
|
-
* says "by", never "runs as", and it carries no permission wording that would imply otherwise.
|
|
134
|
-
*
|
|
135
|
-
* ⚠️ `ownerId` and not the current version's `createdBy`: the owner is written once, from the actor
|
|
136
|
-
* who created the node, and never again — while `createdBy` moves to whoever saved last, which is a
|
|
137
|
-
* different fact and belongs to a different question.
|
|
138
|
-
*
|
|
139
|
-
* ⚠️ Unresolvable means NO line, not "by —". Most ids are unresolvable today (see `useUserName`),
|
|
140
|
-
* and an em dash under every foreign agent's name would be a row of furniture saying nothing.
|
|
141
|
-
*/
|
|
142
|
-
function AgentByline({ node }: { node: Node }) {
|
|
143
|
-
const i18n = useI18n();
|
|
144
|
-
const name = useUserName(node.ownerId);
|
|
145
|
-
if (name === null) return null;
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<span className="block truncate text-xs text-muted-foreground" title={name}>
|
|
149
|
-
{i18n.t("agent.createdBy", { name })}
|
|
150
|
-
</span>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* What somebody without `agents/run` sees instead of the chat and the log.
|
|
156
|
-
*
|
|
157
|
-
* ⚠️ Muted text, not `role="alert"` and not the destructive colour. Nothing is broken and there is
|
|
158
|
-
* nothing to retry: they may read this agent, they may not drive it. A red box would send them to
|
|
159
|
-
* whoever runs the installation over a working system.
|
|
160
|
-
*/
|
|
161
|
-
function NotForYou() {
|
|
162
|
-
const i18n = useI18n();
|
|
163
|
-
return (
|
|
164
|
-
<p className="m-auto max-w-md px-6 py-5 text-center text-sm text-muted-foreground">
|
|
165
|
-
{i18n.t("agent.notPermitted")}
|
|
166
|
-
</p>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* What an agent node from an earlier deployment shows where no runtime is configured (#190).
|
|
172
|
-
*
|
|
173
|
-
* ⚠️ The same muted sentence as `NotForYou`, and for the same reason: nothing is broken and there
|
|
174
|
-
* is nothing to retry. The node and its definition are intact; the installation simply does not run
|
|
175
|
-
* agents, which is the operator's choice, not the reader's problem to debug.
|
|
176
|
-
*/
|
|
177
|
-
function NoRuntime() {
|
|
178
|
-
const i18n = useI18n();
|
|
179
|
-
return (
|
|
180
|
-
<p className="m-auto max-w-md px-6 py-5 text-center text-sm text-muted-foreground">
|
|
181
|
-
{i18n.t("agent.noRuntime")}
|
|
182
|
-
</p>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Whether the agent is working, whether it is switched off, and when it next will be — as a dot
|
|
188
|
-
* beside the name, and only when there is something to say (#253).
|
|
189
|
-
*
|
|
190
|
-
* ⚠️ A dot and not a sentence, and NOT ALWAYS a dot. What stood here was "Idle · no scheduled run"
|
|
191
|
-
* — the absence of a state, spelled out twice, at the most prominent place on the page. `null` from
|
|
192
|
-
* `agentStatus` is the ordinary case and renders nothing at all.
|
|
193
|
-
*
|
|
194
|
-
* ⚠️ The colour is never the only information. The state is the trigger's accessible NAME, the dot
|
|
195
|
-
* itself is `aria-hidden`, and the shapes differ as well as the hues — see `statusDotClass`. A
|
|
196
|
-
* reader who cannot separate two greys, and every reader who is listening rather than looking, gets
|
|
197
|
-
* the same answer as everybody else.
|
|
198
|
-
*
|
|
199
|
-
* ⚠️ "Running" is read from the runtime's own run list, and its absence is NOT read as "idle" — a
|
|
200
|
-
* runtime that cannot be reached says nothing about what the agent is doing.
|
|
201
|
-
*
|
|
202
|
-
* ⚠️ "Next run" is derived from the cron expressions rather than asked. The runtime arms one alarm
|
|
203
|
-
* and publishes neither it nor a schedule, so this is the same reading `AgentCalendar` does — and
|
|
204
|
-
* while the agent is paused it is not shown at all, because a paused agent will not run then.
|
|
205
|
-
*/
|
|
206
|
-
export function AgentStatus({
|
|
207
|
-
agentId,
|
|
208
|
-
definition,
|
|
209
|
-
}: {
|
|
210
|
-
agentId: string;
|
|
211
|
-
definition: AgentDefinition | null;
|
|
212
|
-
}) {
|
|
213
|
-
const { data } = useIntelRouterContext();
|
|
214
|
-
const i18n = useI18n();
|
|
215
|
-
const state = useAgentState(agentId);
|
|
216
|
-
const runs = useQuery({
|
|
217
|
-
queryKey: ["agent-runs", agentId],
|
|
218
|
-
queryFn: () => data.listAgentRuns(agentId),
|
|
219
|
-
// A status dot is not worth a red screen: the runtime being unreachable is reported by the
|
|
220
|
-
// Log tab, which is the view that is actually about runs.
|
|
221
|
-
retry: false,
|
|
222
|
-
});
|
|
223
|
-
const next = nextRunAt(definition, new Date());
|
|
224
|
-
const status = agentStatus({
|
|
225
|
-
runsUnreadable: runs.isError,
|
|
226
|
-
runs: runs.data?.items ?? [],
|
|
227
|
-
paused: state.data?.paused ?? false,
|
|
228
|
-
scheduled: next !== null,
|
|
229
|
-
});
|
|
230
|
-
if (status === null) return null;
|
|
231
|
-
|
|
232
|
-
const when =
|
|
233
|
-
status.kind === "paused"
|
|
234
|
-
? i18n.t("agent.pausedNoRuns")
|
|
235
|
-
: next
|
|
236
|
-
? // ⚠️ Formatted in the SCHEDULE's zone and named with it (#228), not in the reader's.
|
|
237
|
-
// The agent fires at the hour its schedule says; showing that instant in the zone of
|
|
238
|
-
// whoever happens to be looking would put a different hour on screen from the one the
|
|
239
|
-
// agent keeps, and the difference only surfaces as "it ran at the wrong time".
|
|
240
|
-
i18n.t("agent.nextRun", {
|
|
241
|
-
when: new Intl.DateTimeFormat(i18n.locale, {
|
|
242
|
-
dateStyle: "medium",
|
|
243
|
-
timeStyle: "short",
|
|
244
|
-
timeZone: next.timezone,
|
|
245
|
-
}).format(next.at),
|
|
246
|
-
zone: next.timezone,
|
|
247
|
-
})
|
|
248
|
-
: null;
|
|
249
|
-
// ⚠️ A past instant, so it is read in the reader's own zone and names none: unlike a schedule,
|
|
250
|
-
// which fires in the zone it carries, a finished run happened at one moment for everybody.
|
|
251
|
-
const last =
|
|
252
|
-
status.lastRunAt === null
|
|
253
|
-
? null
|
|
254
|
-
: i18n.t("agent.lastRun", {
|
|
255
|
-
when: new Intl.DateTimeFormat(i18n.locale, {
|
|
256
|
-
dateStyle: "medium",
|
|
257
|
-
timeStyle: "short",
|
|
258
|
-
}).format(new Date(status.lastRunAt)),
|
|
259
|
-
});
|
|
260
|
-
const label = [i18n.t(`agent.status.${status.kind}`), last, when].filter(Boolean).join(" · ");
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<TooltipProvider delayDuration={300}>
|
|
264
|
-
<Tooltip>
|
|
265
|
-
{/* A real button, so the dot is reached by Tab and opened without a pointer. */}
|
|
266
|
-
<TooltipTrigger
|
|
267
|
-
type="button"
|
|
268
|
-
aria-label={label}
|
|
269
|
-
className="inline-flex items-center rounded-full p-1 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
270
|
-
>
|
|
271
|
-
<span
|
|
272
|
-
aria-hidden="true"
|
|
273
|
-
className={`block size-2.5 rounded-full ${statusDotClass[status.kind]}`}
|
|
274
|
-
/>
|
|
275
|
-
</TooltipTrigger>
|
|
276
|
-
<TooltipContent className="max-w-xs">{label}</TooltipContent>
|
|
277
|
-
</Tooltip>
|
|
278
|
-
</TooltipProvider>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Pause, and run now.
|
|
284
|
-
*
|
|
285
|
-
* ⚠️ Both are shown even where they cannot be used, against this repo's usual rule that an
|
|
286
|
-
* inapplicable action is absent. The rule is about actions that do not APPLY to a kind — a folder
|
|
287
|
-
* has no retrieval mode. These two apply to every agent, and a head that quietly dropped them would
|
|
288
|
-
* hide from the reader that the agent can be stopped at all. Disabled always carries its reason on
|
|
289
|
-
* the screen, pointed at by `aria-describedby`, so the keyboard hears it too.
|
|
290
|
-
*
|
|
291
|
-
* ⚠️ "Run now" fires a schedule the agent already has; it does not invent one. A run acts with the
|
|
292
|
-
* AGENT's Gate token, so a free choice of target here would let anybody who may drive the agent read
|
|
293
|
-
* with its grants instead of their own — the runtime refuses an undeclared target for that reason,
|
|
294
|
-
* and this menu offers only what it would accept.
|
|
295
|
-
*/
|
|
296
|
-
export function AgentActions({
|
|
297
|
-
agentId,
|
|
298
|
-
definition,
|
|
299
|
-
}: {
|
|
300
|
-
agentId: string;
|
|
301
|
-
definition: AgentDefinition | null;
|
|
302
|
-
}) {
|
|
303
|
-
const { data } = useIntelRouterContext();
|
|
304
|
-
const i18n = useI18n();
|
|
305
|
-
const queryClient = useQueryClient();
|
|
306
|
-
const state = useAgentState(agentId);
|
|
307
|
-
const denied = agentAccessDenied(state.error);
|
|
308
|
-
const paused = state.data?.paused ?? false;
|
|
309
|
-
|
|
310
|
-
const toggle = useMutation({
|
|
311
|
-
mutationFn: async () =>
|
|
312
|
-
paused ? await data.resumeAgent(agentId) : await data.pauseAgent(agentId),
|
|
313
|
-
// The answer is the new state, so the cache is set from it rather than refetched: a round trip
|
|
314
|
-
// would leave a window in which the button still offers what was just done.
|
|
315
|
-
onSuccess: (next) => queryClient.setQueryData(agentStateKey(agentId), next),
|
|
316
|
-
});
|
|
317
|
-
const runNow = useMutation({
|
|
318
|
-
mutationFn: async (target: AgentScheduleTarget) => await data.runAgentNow(agentId, target),
|
|
319
|
-
// The run is worked off the runtime's alarm, so it appears in the list rather than in this
|
|
320
|
-
// answer. Invalidating is what puts it on the Log tab and in the status line.
|
|
321
|
-
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["agent-runs", agentId] }),
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* ⚠️ One entry per TARGET, not per schedule (#323). Two schedules on the same flow — 08:00 and
|
|
326
|
-
* 17:00 — are the ordinary configuration this list exists for, and they produced two menu items
|
|
327
|
-
* nobody could choose between: both start the same run, and the time that tells them apart is
|
|
328
|
-
* not on either. They also shared a React key.
|
|
329
|
-
*
|
|
330
|
-
* Deduplicating is the honest reading of what this button does. "Run now" fires the target
|
|
331
|
-
* immediately; the schedule is the thing it is deliberately NOT waiting for, so which of the two
|
|
332
|
-
* entries somebody picked could never have meant anything. The alternative — two entries labelled
|
|
333
|
-
* with their cron — offers a choice that has no consequence.
|
|
334
|
-
*/
|
|
335
|
-
const targets = [
|
|
336
|
-
...new Map(
|
|
337
|
-
(definition?.schedules ?? []).map((schedule) => [
|
|
338
|
-
`${schedule.target.kind}:${schedule.target.id}`,
|
|
339
|
-
schedule.target,
|
|
340
|
-
]),
|
|
341
|
-
).values(),
|
|
342
|
-
];
|
|
343
|
-
const reason = denied
|
|
344
|
-
? i18n.t("agent.notPermitted")
|
|
345
|
-
: state.isError
|
|
346
|
-
? i18n.t("agent.status.unknown")
|
|
347
|
-
: null;
|
|
348
|
-
const runReason =
|
|
349
|
-
reason ??
|
|
350
|
-
(paused
|
|
351
|
-
? i18n.t("agent.runNowPaused")
|
|
352
|
-
: targets.length === 0
|
|
353
|
-
? i18n.t("agent.runNowNoTarget")
|
|
354
|
-
: null);
|
|
355
|
-
const failure = toggle.error ?? runNow.error;
|
|
356
|
-
|
|
357
|
-
return (
|
|
358
|
-
<span className="flex items-center gap-2">
|
|
359
|
-
{/* ⚠️ `sr-only` and no longer beside the buttons (#253): the reason belongs in the tooltip of
|
|
360
|
-
the control it disables, not as a sentence in the title line. But it stays IN THE
|
|
361
|
-
ACCESSIBILITY TREE — `hidden` or `display: none` would leave `aria-describedby` pointing at
|
|
362
|
-
nothing, and Radix puts the tooltip's own content there only while it is open. So the words
|
|
363
|
-
are here for the keyboard and on the hover for the pointer, and never in the bar. */}
|
|
364
|
-
{runReason ? (
|
|
365
|
-
<span id="agent-actions-reason" className="sr-only">
|
|
366
|
-
{runReason}
|
|
367
|
-
</span>
|
|
368
|
-
) : null}
|
|
369
|
-
{/* ⚠️ An agent with no key in its Durable Object fails every run the same way forever, so
|
|
370
|
-
"try again" is advice that cannot work (#200). The one refusal with a repair path says
|
|
371
|
-
what the repair is, and names where it lives — the profile, not this bar (D29).
|
|
372
|
-
|
|
373
|
-
⚠️ Every other refusal is shown in its OWN words where it brought any (#201). `Resume`
|
|
374
|
-
fails for reasons only the runtime knows, and "that did not work" turns each of them into
|
|
375
|
-
the same shrug — which is how a resume that could not arm the alarm looked like one that
|
|
376
|
-
simply needed another click. */}
|
|
377
|
-
{failure ? (
|
|
378
|
-
<span role="alert" className="text-xs text-destructive max-sm:sr-only sm:inline">
|
|
379
|
-
{agentKeyMissing(failure)
|
|
380
|
-
? i18n.t("agent.keyMissing")
|
|
381
|
-
: (agentProblemDetail(failure) ?? i18n.t("agent.actionFailed"))}
|
|
382
|
-
</span>
|
|
383
|
-
) : null}
|
|
384
|
-
<WhyDisabled reason={reason}>
|
|
385
|
-
<button
|
|
386
|
-
type="button"
|
|
387
|
-
onClick={() => toggle.mutate()}
|
|
388
|
-
disabled={reason !== null || state.isPending || toggle.isPending}
|
|
389
|
-
aria-busy={toggle.isPending}
|
|
390
|
-
{...(reason ? { "aria-describedby": "agent-actions-reason" } : {})}
|
|
391
|
-
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"
|
|
392
|
-
>
|
|
393
|
-
{paused ? (
|
|
394
|
-
<Play aria-hidden="true" className="size-4" />
|
|
395
|
-
) : (
|
|
396
|
-
<Pause aria-hidden="true" className="size-4" />
|
|
397
|
-
)}
|
|
398
|
-
{i18n.t(paused ? "agent.resume" : "agent.pause")}
|
|
399
|
-
</button>
|
|
400
|
-
</WhyDisabled>
|
|
401
|
-
<WhyDisabled reason={runReason}>
|
|
402
|
-
<RunNow
|
|
403
|
-
targets={targets}
|
|
404
|
-
disabled={runReason !== null || runNow.isPending}
|
|
405
|
-
busy={runNow.isPending}
|
|
406
|
-
describedBy={runReason ? "agent-actions-reason" : undefined}
|
|
407
|
-
start={(target) => runNow.mutate(target)}
|
|
408
|
-
/>
|
|
409
|
-
</WhyDisabled>
|
|
410
|
-
</span>
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* The reason a control is shut, on its own hover instead of beside it (#253).
|
|
416
|
-
*
|
|
417
|
-
* ⚠️ The wrapping span is not decoration. A disabled button fires no pointer events, so a trigger
|
|
418
|
-
* bound directly to it never opens — the same trick the profile's disabled "Add contact" needs, and
|
|
419
|
-
* the reason both keep an `sr-only` paragraph for `aria-describedby` beside the tooltip.
|
|
420
|
-
*
|
|
421
|
-
* ⚠️ The wrapper is ALWAYS here, and only the content comes and goes. A reason appears and vanishes
|
|
422
|
-
* as the state query settles, and a version of this that returned the bare child when there was
|
|
423
|
-
* nothing to say re-created the button on every such change — which throws away its focus and, in a
|
|
424
|
-
* test, leaves whoever grabbed it holding a node that is no longer on screen. Without content Radix
|
|
425
|
-
* opens nothing at all, so the empty case costs a span and shows no bubble.
|
|
426
|
-
*/
|
|
427
|
-
function WhyDisabled({ reason, children }: { reason: string | null; children: React.ReactNode }) {
|
|
428
|
-
return (
|
|
429
|
-
<TooltipProvider delayDuration={300}>
|
|
430
|
-
<Tooltip>
|
|
431
|
-
<TooltipTrigger asChild>
|
|
432
|
-
<span className="inline-flex">{children}</span>
|
|
433
|
-
</TooltipTrigger>
|
|
434
|
-
{reason === null ? null : <TooltipContent className="max-w-xs">{reason}</TooltipContent>}
|
|
435
|
-
</Tooltip>
|
|
436
|
-
</TooltipProvider>
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* ⚠️ One schedule runs straight away; several open a menu. A button that silently picked the first
|
|
442
|
-
* of three targets would run the wrong thing at a moment somebody chose deliberately, and a menu in
|
|
443
|
-
* front of a single obvious target is a click that answers nothing.
|
|
444
|
-
*/
|
|
445
|
-
function RunNow({
|
|
446
|
-
targets,
|
|
447
|
-
disabled,
|
|
448
|
-
busy,
|
|
449
|
-
describedBy,
|
|
450
|
-
start,
|
|
451
|
-
}: {
|
|
452
|
-
targets: AgentScheduleTarget[];
|
|
453
|
-
disabled: boolean;
|
|
454
|
-
busy: boolean;
|
|
455
|
-
describedBy: string | undefined;
|
|
456
|
-
start(target: AgentScheduleTarget): void;
|
|
457
|
-
}) {
|
|
458
|
-
const i18n = useI18n();
|
|
459
|
-
const only = targets.length === 1 ? targets[0] : undefined;
|
|
460
|
-
const className =
|
|
461
|
-
"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";
|
|
462
|
-
|
|
463
|
-
if (targets.length > 1) {
|
|
464
|
-
return (
|
|
465
|
-
<DropdownMenu>
|
|
466
|
-
<DropdownMenuTrigger asChild>
|
|
467
|
-
<button
|
|
468
|
-
type="button"
|
|
469
|
-
disabled={disabled}
|
|
470
|
-
aria-busy={busy}
|
|
471
|
-
{...(describedBy ? { "aria-describedby": describedBy } : {})}
|
|
472
|
-
className={className}
|
|
473
|
-
>
|
|
474
|
-
<Play aria-hidden="true" className="size-4" />
|
|
475
|
-
{i18n.t("agent.runNow")}
|
|
476
|
-
</button>
|
|
477
|
-
</DropdownMenuTrigger>
|
|
478
|
-
<DropdownMenuContent align="end">
|
|
479
|
-
{targets.map((target) => (
|
|
480
|
-
<DropdownMenuItem key={`${target.kind}:${target.id}`} onSelect={() => start(target)}>
|
|
481
|
-
<TargetLabel target={target} />
|
|
482
|
-
</DropdownMenuItem>
|
|
483
|
-
))}
|
|
484
|
-
</DropdownMenuContent>
|
|
485
|
-
</DropdownMenu>
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<button
|
|
491
|
-
type="button"
|
|
492
|
-
onClick={() => only && start(only)}
|
|
493
|
-
disabled={disabled || !only}
|
|
494
|
-
aria-busy={busy}
|
|
495
|
-
{...(describedBy ? { "aria-describedby": describedBy } : {})}
|
|
496
|
-
className={className}
|
|
497
|
-
>
|
|
498
|
-
<Play aria-hidden="true" className="size-4" />
|
|
499
|
-
{i18n.t("agent.runNow")}
|
|
500
|
-
</button>
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function TargetLabel({ target }: { target: AgentScheduleTarget }) {
|
|
505
|
-
const { title } = useEntryTitle(target.id, target.kind === "flow");
|
|
506
|
-
return <span className="truncate">{title}</span>;
|
|
507
|
-
}
|