@anchrd/intel-ui 0.14.0 → 0.16.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 +3 -2
- package/src/agent/agent-calendar/agent-calendar.tsx +42 -4
- package/src/agent/agent-costs/agent-costs.ts +95 -0
- package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +48 -0
- package/src/agent/agent-entry-title/agent-entry-title.ts +48 -7
- package/src/agent/agent-log/agent-log.tsx +162 -16
- package/src/agent/agent-models/agent-models.ts +31 -67
- package/src/agent/agent-models/agent-models.types.ts +11 -5
- package/src/agent/agent-profile/agent-profile.tsx +237 -51
- package/src/agent/agent-state/agent-state.ts +17 -0
- package/src/agent/agent-status-dot/agent-status-dot.ts +73 -0
- package/src/agent/agent.tsx +159 -50
- package/src/app/app-tree/app-tree.tsx +9 -3
- package/src/app/settings-dialog/settings-dialog.tsx +13 -4
- package/src/data/agent-runtime/agent-runtime.ts +38 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +20 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +19 -0
- package/src/entry-picker/entry-picker.tsx +16 -5
- package/src/hooks/use-model-catalog.ts +26 -0
- package/src/i18n/de.json +30 -10
- package/src/i18n/en.json +30 -10
- package/src/i18n/es.json +30 -10
- package/src/section-hint/section-hint.tsx +40 -0
- package/src/timezone/timezone-context.tsx +25 -5
- package/src/timezone/timezone.ts +20 -5
- package/src/title-row/title-row.tsx +9 -4
- package/src/user-name/user-name.ts +41 -0
- package/tsconfig.json +7 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AgentModel } from "@anchrd/intel-contract";
|
|
1
|
+
import { AgentModel, type ModelCatalogEntry } from "@anchrd/intel-contract";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import type { ModelFacts
|
|
3
|
+
import type { ModelFacts } from "./agent-models.types.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Which models this installation offers.
|
|
@@ -16,65 +16,6 @@ import type { ModelFacts, ModelPrice } from "./agent-models.types.ts";
|
|
|
16
16
|
*/
|
|
17
17
|
const ConfiguredModels = z.array(AgentModel);
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Context window and price per model, collected 2026-08-06 from
|
|
21
|
-
* `GET /accounts/{id}/ai/models/search` (Workers AI, every entry carrying `function_calling`) and
|
|
22
|
-
* from Anthropic's model list. Both figures are shown beside the select, so this table is the one
|
|
23
|
-
* place to correct when they move.
|
|
24
|
-
*
|
|
25
|
-
* ⚠️ It is a copy and it ages silently. Cloudflare changes context windows and prices without
|
|
26
|
-
* notice, and a wrong price in the interface is worse than none — re-read the endpoint above rather
|
|
27
|
-
* than editing a number from memory. Asking live from the browser is not the way out: the account
|
|
28
|
-
* token does not belong in a SPA.
|
|
29
|
-
*
|
|
30
|
-
* ⚠️ Workers AI models that do not carry `function_calling` are deliberately absent. An agent is a
|
|
31
|
-
* tool loop; a model that cannot call a tool cannot run one, and offering it would only produce a
|
|
32
|
-
* broken agent for whoever picked it.
|
|
33
|
-
*/
|
|
34
|
-
const catalog: Record<string, { name: string; contextTokens: number; price: ModelPrice }> = {
|
|
35
|
-
"anthropic:claude-opus-5": price("Opus 5", 1_000_000, 5, 25),
|
|
36
|
-
"anthropic:claude-sonnet-5": price("Sonnet 5", 1_000_000, 3, 15),
|
|
37
|
-
"anthropic:claude-sonnet-4": price("Sonnet 4", 200_000, 3, 15),
|
|
38
|
-
"anthropic:claude-haiku-4-5": price("Haiku 4.5", 200_000, 1, 5),
|
|
39
|
-
"workers-ai:@cf/openai/gpt-oss-120b": price("GPT-OSS 120B", 128_000, 0.35, 0.75),
|
|
40
|
-
"workers-ai:@cf/openai/gpt-oss-20b": price("GPT-OSS 20B", 128_000, 0.2, 0.3),
|
|
41
|
-
"workers-ai:@cf/moonshotai/kimi-k2.6": price("Kimi K2.6", 262_144, 0.95, 4),
|
|
42
|
-
"workers-ai:@cf/moonshotai/kimi-k2.7-code": price("Kimi K2.7 Code", 262_144, 0.95, 4),
|
|
43
|
-
"workers-ai:@cf/zai-org/glm-5.2": price("GLM 5.2", 262_144, 1.4, 4.4),
|
|
44
|
-
"workers-ai:@cf/zai-org/glm-4.7-flash": price("GLM 4.7 Flash", 131_072, 0.0605, 0.4),
|
|
45
|
-
"workers-ai:@cf/google/gemma-4-26b-a4b-it": price("Gemma 4 26B", 256_000, 0.1, 0.3),
|
|
46
|
-
"workers-ai:@cf/nvidia/nemotron-3-120b-a12b": price("Nemotron 3 120B", 256_000, 0.5, 1.5),
|
|
47
|
-
"workers-ai:@cf/meta/llama-4-scout-17b-16e-instruct": price("Llama 4 Scout", 131_000, 0.27, 0.85),
|
|
48
|
-
"workers-ai:@cf/meta/llama-3.3-70b-instruct-fp8-fast": price(
|
|
49
|
-
"Llama 3.3 70B Fast",
|
|
50
|
-
24_000,
|
|
51
|
-
0.293,
|
|
52
|
-
2.253,
|
|
53
|
-
),
|
|
54
|
-
"workers-ai:@cf/mistralai/mistral-small-3.1-24b-instruct": price(
|
|
55
|
-
"Small 3.1 24B",
|
|
56
|
-
128_000,
|
|
57
|
-
0.351,
|
|
58
|
-
0.555,
|
|
59
|
-
),
|
|
60
|
-
"workers-ai:@cf/ibm-granite/granite-4.0-h-micro": price(
|
|
61
|
-
"Granite 4.0 Micro",
|
|
62
|
-
131_000,
|
|
63
|
-
0.017,
|
|
64
|
-
0.112,
|
|
65
|
-
),
|
|
66
|
-
"workers-ai:@cf/qwen/qwen3-30b-a3b-fp8": price("Qwen3 30B", 32_768, 0.0509, 0.335),
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function price(
|
|
70
|
-
name: string,
|
|
71
|
-
contextTokens: number,
|
|
72
|
-
inputPerMillion: number,
|
|
73
|
-
outputPerMillion: number,
|
|
74
|
-
): { name: string; contextTokens: number; price: ModelPrice } {
|
|
75
|
-
return { name, contextTokens, price: { inputPerMillion, outputPerMillion } };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
19
|
/**
|
|
79
20
|
* ⚠️ The brand comes from the model id, never from `provider`. `provider` says who serves the model,
|
|
80
21
|
* so every Workers AI entry would read "Cloudflare" — thirteen identical labels in a list whose whole
|
|
@@ -131,20 +72,43 @@ export function parseModelKey(key: string): AgentModel | null {
|
|
|
131
72
|
return parsed.success ? parsed.data : null;
|
|
132
73
|
}
|
|
133
74
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
75
|
+
/**
|
|
76
|
+
* How a model is written for a person: brand first, then the model's own name, then the two numbers
|
|
77
|
+
* somebody actually chooses on.
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ The figures come IN, from intel's catalog (#257). There is no table in this package any more,
|
|
80
|
+
* and putting one back would restore exactly what the removed one was accused of: it was a copy, it
|
|
81
|
+
* aged silently, and a wrong price in the interface is worse than none. What this file still owns is
|
|
82
|
+
* how a model is written down — the brand, the fallback name, the two formats — none of which is a
|
|
83
|
+
* fact about money.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ A missing catalog and an unknown model produce the SAME answer: a name and no figures. The
|
|
86
|
+
* select therefore renders while the catalog is still on its way, and it renders at all when the
|
|
87
|
+
* catalog could not be fetched — an agent whose model cannot be chosen is an agent nobody can repair
|
|
88
|
+
* from this screen.
|
|
89
|
+
*/
|
|
90
|
+
export function modelFacts(model: AgentModel, catalog?: ModelCatalogEntry[]): ModelFacts {
|
|
91
|
+
const known = catalog?.find(
|
|
92
|
+
(entry) => entry.provider === model.provider && entry.model === model.model,
|
|
93
|
+
);
|
|
139
94
|
if (known) {
|
|
140
95
|
return {
|
|
141
96
|
brand: brandOf(model),
|
|
142
97
|
name: known.name,
|
|
143
98
|
contextTokens: known.contextTokens,
|
|
144
99
|
price: known.price,
|
|
100
|
+
// Nothing is stale when there is nothing to be stale: a model the catalog carries no price
|
|
101
|
+
// for shows no price, and no warning about a price it never showed.
|
|
102
|
+
stale: known.source === "builtin" && known.price !== null,
|
|
145
103
|
};
|
|
146
104
|
}
|
|
147
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
brand: brandOf(model),
|
|
107
|
+
name: unknownName(model),
|
|
108
|
+
contextTokens: null,
|
|
109
|
+
price: null,
|
|
110
|
+
stale: false,
|
|
111
|
+
};
|
|
148
112
|
}
|
|
149
113
|
|
|
150
114
|
function brandOf(model: AgentModel): string {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModelPrice } from "@anchrd/intel-contract";
|
|
2
|
+
|
|
1
3
|
/** What a reader needs in order to pick a model: whose it is, what it is called, and what it costs. */
|
|
2
4
|
export interface ModelFacts {
|
|
3
5
|
brand: string;
|
|
@@ -9,10 +11,14 @@ export interface ModelFacts {
|
|
|
9
11
|
*/
|
|
10
12
|
contextTokens: number | null;
|
|
11
13
|
price: ModelPrice | null;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the two figures came off a written-out table rather than from Cloudflare (#257).
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ It exists so the screen can SAY so. A silent fallback onto typed-out prices is the state
|
|
18
|
+
* this feature replaced, with more code — and a wrong price in the interface is worse than none.
|
|
19
|
+
* `false` for a model with no figures at all: there is nothing there to be stale.
|
|
20
|
+
*/
|
|
21
|
+
stale: boolean;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
export interface ModelPrice {
|
|
16
|
-
inputPerMillion: number;
|
|
17
|
-
outputPerMillion: number;
|
|
18
|
-
}
|
|
24
|
+
export type { ModelPrice };
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AgentCosts,
|
|
3
|
+
type AgentCostWindow,
|
|
2
4
|
type AgentDefinition,
|
|
3
5
|
type AgentReference,
|
|
6
|
+
AgentReferenceKinds,
|
|
4
7
|
AgentReferenceRole,
|
|
5
8
|
type AgentSchedule,
|
|
9
|
+
agentReferenceRolesFor,
|
|
6
10
|
type Node,
|
|
11
|
+
type NodeKind,
|
|
7
12
|
} from "@anchrd/intel-contract";
|
|
8
13
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
9
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Bot,
|
|
16
|
+
Check,
|
|
17
|
+
Copy,
|
|
18
|
+
FileText,
|
|
19
|
+
Folder,
|
|
20
|
+
KeyRound,
|
|
21
|
+
MessageSquare,
|
|
22
|
+
Paperclip,
|
|
23
|
+
Plus,
|
|
24
|
+
Table2,
|
|
25
|
+
Trash2,
|
|
26
|
+
Wrench,
|
|
27
|
+
} from "lucide-react";
|
|
10
28
|
import { useState } from "react";
|
|
29
|
+
import { costWindow, formatCost, useAgentCosts } from "@/agent/agent-costs/agent-costs.ts";
|
|
11
30
|
import { type AgentDefinitionHandle, asDraft } from "@/agent/agent-definition/agent-definition.ts";
|
|
31
|
+
import { delegationNotice } from "@/agent/agent-delegation-notice/agent-delegation-notice.ts";
|
|
12
32
|
import { useEntryTitle } from "@/agent/agent-entry-title/agent-entry-title.ts";
|
|
13
33
|
import {
|
|
14
34
|
formatContextTokens,
|
|
@@ -29,11 +49,14 @@ import {
|
|
|
29
49
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
30
50
|
import { agentMcpAddress } from "@/data/agent-runtime/agent-runtime.ts";
|
|
31
51
|
import { EntryPicker, type PickerKind } from "@/entry-picker/entry-picker.tsx";
|
|
52
|
+
import { useModelCatalog } from "@/hooks/use-model-catalog.ts";
|
|
32
53
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
33
54
|
import { Modal } from "@/modal/modal.tsx";
|
|
34
55
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
56
|
+
import { SectionHint } from "@/section-hint/section-hint.tsx";
|
|
35
57
|
import { TimezoneCombobox } from "@/timezone/timezone-combobox/timezone-combobox.tsx";
|
|
36
58
|
import { useTimezone } from "@/timezone/timezone-context.tsx";
|
|
59
|
+
import { useSessionUser, useUserName } from "@/user-name/user-name.ts";
|
|
37
60
|
|
|
38
61
|
// No dividers between sections — the space is the separation (#204). The explanation sits behind
|
|
39
62
|
// the icon rather than under the title, so the page shows what the agent IS and keeps the prose
|
|
@@ -58,26 +81,6 @@ function Section({
|
|
|
58
81
|
);
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
// ⚠️ The hint is the trigger's accessible name, not only the tooltip's content: Radix describes the
|
|
62
|
-
// trigger by the content only while it is open, and a listener who tabs past a nameless icon button
|
|
63
|
-
// would never learn there is anything behind it.
|
|
64
|
-
function SectionHint({ hint }: { hint: string }) {
|
|
65
|
-
return (
|
|
66
|
-
<TooltipProvider delayDuration={300}>
|
|
67
|
-
<Tooltip>
|
|
68
|
-
<TooltipTrigger
|
|
69
|
-
type="button"
|
|
70
|
-
aria-label={hint}
|
|
71
|
-
className="rounded-full text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
72
|
-
>
|
|
73
|
-
<Info aria-hidden="true" className="size-3.5" />
|
|
74
|
-
</TooltipTrigger>
|
|
75
|
-
<TooltipContent className="max-w-xs">{hint}</TooltipContent>
|
|
76
|
-
</Tooltip>
|
|
77
|
-
</TooltipProvider>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
84
|
/**
|
|
82
85
|
* Everything an agent is, on one page: how to reach it, what it reads, what it may call, when it
|
|
83
86
|
* acts on its own, which model does the thinking, and who it is in Gate.
|
|
@@ -131,7 +134,7 @@ export function AgentProfile({ node, agent }: { node: Node; agent: AgentDefiniti
|
|
|
131
134
|
<KnowledgeSection definition={definition} agent={agent} />
|
|
132
135
|
<ToolsSection definition={definition} agent={agent} />
|
|
133
136
|
<ScheduleSection definition={definition} agent={agent} />
|
|
134
|
-
<ModelSection definition={definition} agent={agent} />
|
|
137
|
+
<ModelSection definition={definition} agent={agent} agentId={node.id} />
|
|
135
138
|
<IdentitySection node={node} agent={agent} />
|
|
136
139
|
</div>
|
|
137
140
|
);
|
|
@@ -310,6 +313,24 @@ function ContactSection({ node }: { node: Node }) {
|
|
|
310
313
|
);
|
|
311
314
|
}
|
|
312
315
|
|
|
316
|
+
// Everything any role accepts, which is what the picker may offer. Derived from the contract's one
|
|
317
|
+
// table rather than written out again — a second list here would drift.
|
|
318
|
+
const pickableKinds: NodeKind[] = [
|
|
319
|
+
...new Set(Object.values(AgentReferenceKinds).flat()),
|
|
320
|
+
] as NodeKind[];
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The role a freshly picked entry starts as — the widest thing that kind can honestly be.
|
|
324
|
+
*
|
|
325
|
+
* A folder starts as search space, the way it always has; a document or a table starts as the
|
|
326
|
+
* system message, which is the only role that reads a single node at all. `null` for a flow and for
|
|
327
|
+
* a kind no role accepts, and the caller then adds nothing rather than inventing a role.
|
|
328
|
+
*/
|
|
329
|
+
function defaultRoleFor(kind: PickerKind): AgentReferenceRole | null {
|
|
330
|
+
if (kind === "flow") return null;
|
|
331
|
+
return kind === "folder" ? "semantic-context" : (agentReferenceRolesFor(kind)[0] ?? null);
|
|
332
|
+
}
|
|
333
|
+
|
|
313
334
|
/**
|
|
314
335
|
* What the agent reads, and in which capacity.
|
|
315
336
|
*
|
|
@@ -317,6 +338,14 @@ function ContactSection({ node }: { node: Node }) {
|
|
|
317
338
|
* explicitly, and the reason is that the three roles do very different things — a folder read as
|
|
318
339
|
* the system message becomes instructions, one read as memory is written back to. Something that
|
|
319
340
|
* changes what a document MEANS to an agent must look changeable.
|
|
341
|
+
*
|
|
342
|
+
* ⚠️ Since #255 a row can also be a single document or table, and the role select shows only the
|
|
343
|
+
* roles that kind can carry. `memory` and `semantic-context` stay folders — the agent searches both
|
|
344
|
+
* and writes into one of them — so a document simply never offers them.
|
|
345
|
+
*
|
|
346
|
+
* ⚠️ Memory is searched too, since #259, and the hint beside this section says so in words (#269).
|
|
347
|
+
* The role name alone reads as a write-only notebook, and somebody who believes that files things
|
|
348
|
+
* there that come back at them out of every later search.
|
|
320
349
|
*/
|
|
321
350
|
function KnowledgeSection({
|
|
322
351
|
definition,
|
|
@@ -370,12 +399,20 @@ function KnowledgeSection({
|
|
|
370
399
|
</button>
|
|
371
400
|
{adding ? (
|
|
372
401
|
<Modal title={i18n.t("agent.addReference")} close={() => setAdding(false)}>
|
|
402
|
+
{/* ⚠️ The picker offers the UNION of what any role accepts, and the row's role select
|
|
403
|
+
then offers only the roles that accept what was picked (#255). The rule is asked in
|
|
404
|
+
this direction because the thing is chosen first: a role chosen up front would mean a
|
|
405
|
+
second dialog before the picker, and a picker that let anything be chosen and refused
|
|
406
|
+
afterwards is worse than one that never offered it. Either way no illegal pair is
|
|
407
|
+
reachable — `agentReferenceKinds` is the one list both sides read. */}
|
|
373
408
|
<EntryPicker
|
|
374
|
-
kind=
|
|
409
|
+
kind={pickableKinds}
|
|
375
410
|
value=""
|
|
376
|
-
label={i18n.t("agent.
|
|
377
|
-
onSelect={(nodeId) => {
|
|
378
|
-
|
|
411
|
+
label={i18n.t("agent.pickEntry")}
|
|
412
|
+
onSelect={(nodeId, entryKind) => {
|
|
413
|
+
const role = defaultRoleFor(entryKind);
|
|
414
|
+
if (role === null) return;
|
|
415
|
+
write([...definition.references, { nodeId, role }]);
|
|
379
416
|
setAdding(false);
|
|
380
417
|
}}
|
|
381
418
|
/>
|
|
@@ -385,13 +422,31 @@ function KnowledgeSection({
|
|
|
385
422
|
);
|
|
386
423
|
}
|
|
387
424
|
|
|
425
|
+
// What each kind looks like at a glance. An `attachment` and an `agent` cannot be picked, but an
|
|
426
|
+
// older definition may still name one, so every kind has a mark rather than a fallback that would
|
|
427
|
+
// draw two different things the same way.
|
|
428
|
+
const KindIcon: Record<NodeKind, typeof Folder> = {
|
|
429
|
+
folder: Folder,
|
|
430
|
+
document: FileText,
|
|
431
|
+
table: Table2,
|
|
432
|
+
attachment: Paperclip,
|
|
433
|
+
agent: Bot,
|
|
434
|
+
};
|
|
435
|
+
|
|
388
436
|
/**
|
|
389
|
-
* One
|
|
437
|
+
* One thing the agent reads, where it sits, and what it counts as.
|
|
390
438
|
*
|
|
391
|
-
* ⚠️ Its own component so the select can be NAMED after the
|
|
392
|
-
* "What this
|
|
393
|
-
*
|
|
394
|
-
*
|
|
439
|
+
* ⚠️ Its own component so the select can be NAMED after the entry. Three rows carrying the same
|
|
440
|
+
* "What this counts as" are three identical controls to anybody listening rather than looking — the
|
|
441
|
+
* name is beside them on screen, and the screen is exactly what that reader does not have. It needs
|
|
442
|
+
* the resolved title, which is why the lookup is a hook rather than a component here.
|
|
443
|
+
*
|
|
444
|
+
* ⚠️ The path is not decoration (#255). Two folders called `Notes` in different corners of the tree
|
|
445
|
+
* were indistinguishable here, and this is the screen on which somebody decides what an agent may
|
|
446
|
+
* read and write into.
|
|
447
|
+
*
|
|
448
|
+
* ⚠️ The kind is a symbol AND a word. The symbol is for the eye and the `sr-only` word is for
|
|
449
|
+
* everyone else — an icon with no text is a row that says "Notes" three times to a screen reader.
|
|
395
450
|
*/
|
|
396
451
|
function ReferenceRow({
|
|
397
452
|
reference,
|
|
@@ -403,11 +458,31 @@ function ReferenceRow({
|
|
|
403
458
|
remove(): void;
|
|
404
459
|
}) {
|
|
405
460
|
const i18n = useI18n();
|
|
406
|
-
const { title, known } = useEntryTitle(reference.nodeId);
|
|
461
|
+
const { title, known, kind, path } = useEntryTitle(reference.nodeId);
|
|
462
|
+
const Icon = kind === null ? null : KindIcon[kind];
|
|
463
|
+
// ⚠️ Only the roles this kind can carry, so an impossible pair is never offered — and the current
|
|
464
|
+
// role is kept in the list whatever it is, or an older definition would open on an empty select
|
|
465
|
+
// and the first touch of the control would silently change what the agent does.
|
|
466
|
+
const roles = [
|
|
467
|
+
...new Set([
|
|
468
|
+
...(kind === null ? AgentReferenceRole.options : agentReferenceRolesFor(kind)),
|
|
469
|
+
reference.role,
|
|
470
|
+
]),
|
|
471
|
+
];
|
|
472
|
+
|
|
407
473
|
return (
|
|
408
474
|
<li className="flex flex-wrap items-center gap-3 rounded-lg border bg-card px-4 py-3">
|
|
409
|
-
|
|
410
|
-
|
|
475
|
+
{Icon ? <Icon aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" /> : null}
|
|
476
|
+
<span className={`min-w-0 flex-1${known ? "" : " text-muted-foreground"}`}>
|
|
477
|
+
<span className="block truncate text-sm">
|
|
478
|
+
{title}
|
|
479
|
+
{kind === null ? null : <span className="sr-only"> — {i18n.t(`node.kind.${kind}`)}</span>}
|
|
480
|
+
</span>
|
|
481
|
+
{path.length > 0 ? (
|
|
482
|
+
<span className="block truncate text-xs text-muted-foreground" title={path.join(" / ")}>
|
|
483
|
+
{path.join(" / ")}
|
|
484
|
+
</span>
|
|
485
|
+
) : null}
|
|
411
486
|
</span>
|
|
412
487
|
<Select
|
|
413
488
|
value={reference.role}
|
|
@@ -420,7 +495,7 @@ function ReferenceRow({
|
|
|
420
495
|
<SelectValue />
|
|
421
496
|
</SelectTrigger>
|
|
422
497
|
<SelectContent>
|
|
423
|
-
{
|
|
498
|
+
{roles.map((role) => (
|
|
424
499
|
<SelectItem key={role} value={role}>
|
|
425
500
|
{i18n.t(`agent.role.${role}`)}
|
|
426
501
|
</SelectItem>
|
|
@@ -459,9 +534,10 @@ function EntryTitle({ entryId, flow = false }: { entryId: string; flow?: boolean
|
|
|
459
534
|
*
|
|
460
535
|
* ⚠️ The notice is not decoration, and it stands whether or not anything is listed yet. A shared
|
|
461
536
|
* agent runs on the DELEGATOR's connection whoever starts it, which is a real widening of who can
|
|
462
|
-
* act as this person
|
|
463
|
-
*
|
|
464
|
-
*
|
|
537
|
+
* act as this person. It is no longer a paragraph under the list (#254) — it said the same thing on
|
|
538
|
+
* every visit at the place where somebody was doing something else — but it is not gone either: it
|
|
539
|
+
* is the second half of this section's own hint, so it is in the trigger's accessible name and a
|
|
540
|
+
* keyboard reaches it, and it still stands in the picker where the delegation actually happens.
|
|
465
541
|
*/
|
|
466
542
|
function ToolsSection({
|
|
467
543
|
definition,
|
|
@@ -472,6 +548,9 @@ function ToolsSection({
|
|
|
472
548
|
}) {
|
|
473
549
|
const { data } = useIntelRouterContext();
|
|
474
550
|
const i18n = useI18n();
|
|
551
|
+
const me = useSessionUser();
|
|
552
|
+
const delegatedBy = definition.tools?.delegatedBy ?? null;
|
|
553
|
+
const delegatorName = useUserName(delegatedBy);
|
|
475
554
|
const [adding, setAdding] = useState(false);
|
|
476
555
|
const servers = useQuery({ queryKey: ["tool-servers"], queryFn: () => data.listToolServers() });
|
|
477
556
|
const selected = definition.tools?.servers ?? [];
|
|
@@ -488,8 +567,17 @@ function ToolsSection({
|
|
|
488
567
|
(server) => !selected.includes(server.handle),
|
|
489
568
|
);
|
|
490
569
|
|
|
570
|
+
// Two whole sentences joined, never one sentence built from two fragments: each stays its own
|
|
571
|
+
// catalog entry and each translates on its own. What the join fixes is only their order.
|
|
572
|
+
const notice = delegationNotice(i18n, {
|
|
573
|
+
delegatedBy,
|
|
574
|
+
viewerId: me?.id ?? null,
|
|
575
|
+
delegatorName,
|
|
576
|
+
});
|
|
577
|
+
const hint = `${i18n.t("agent.toolsHint")} ${notice}`;
|
|
578
|
+
|
|
491
579
|
return (
|
|
492
|
-
<Section title={i18n.t("agent.tools")} hint={
|
|
580
|
+
<Section title={i18n.t("agent.tools")} hint={hint}>
|
|
493
581
|
{selected.length === 0 ? null : (
|
|
494
582
|
<ul className="space-y-2">
|
|
495
583
|
{selected.map((handle) => {
|
|
@@ -534,7 +622,6 @@ function ToolsSection({
|
|
|
534
622
|
<Plus aria-hidden="true" className="size-4" />
|
|
535
623
|
{i18n.t("agent.addTool")}
|
|
536
624
|
</button>
|
|
537
|
-
<p className="mt-3 text-xs text-muted-foreground">{i18n.t("agent.toolsDelegationNotice")}</p>
|
|
538
625
|
{adding ? (
|
|
539
626
|
<Modal title={i18n.t("agent.addTool")} close={() => setAdding(false)}>
|
|
540
627
|
{servers.isPending ? (
|
|
@@ -569,9 +656,10 @@ function ToolsSection({
|
|
|
569
656
|
))}
|
|
570
657
|
</ul>
|
|
571
658
|
)}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
659
|
+
{/* ⚠️ Here it stays a visible paragraph, unlike the section's own (#254). This is the
|
|
660
|
+
moment the delegation is actually made, and a consequence that is only reachable by
|
|
661
|
+
hovering an icon somewhere behind the dialog is a consequence nobody reads. */}
|
|
662
|
+
<p className="mt-4 text-xs text-muted-foreground">{notice}</p>
|
|
575
663
|
</Modal>
|
|
576
664
|
) : null}
|
|
577
665
|
</Section>
|
|
@@ -687,16 +775,24 @@ function ScheduleDialog({ close, add }: { close(): void; add(schedule: AgentSche
|
|
|
687
775
|
{i18n.t("agent.cronHint")}
|
|
688
776
|
</span>
|
|
689
777
|
</label>
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
778
|
+
{/* ⚠️ The hint is behind the ⓘ (#249) and ALSO `sr-only` below. The two are not a
|
|
779
|
+
duplicate: the tooltip's content is in the accessibility tree only while it is open, so
|
|
780
|
+
the field would lose the description `aria-describedby` promises the moment the sentence
|
|
781
|
+
became a hover. Route (1) of the ticket, and the cheaper of the two — nothing has to be
|
|
782
|
+
argued about a description that is simply still there. */}
|
|
783
|
+
<div className="flex items-center gap-1.5">
|
|
784
|
+
<label className="block text-sm font-medium" htmlFor="agent-schedule-timezone">
|
|
785
|
+
{i18n.t("agent.scheduleTimezone")}
|
|
786
|
+
</label>
|
|
787
|
+
<SectionHint hint={i18n.t("agent.scheduleTimezoneHint")} />
|
|
788
|
+
</div>
|
|
693
789
|
<TimezoneCombobox
|
|
694
790
|
id="agent-schedule-timezone"
|
|
695
791
|
value={timezone}
|
|
696
792
|
onChange={setTimezone}
|
|
697
793
|
describedBy="agent-timezone-hint"
|
|
698
794
|
/>
|
|
699
|
-
<span id="agent-timezone-hint" className="-
|
|
795
|
+
<span id="agent-timezone-hint" className="sr-only">
|
|
700
796
|
{i18n.t("agent.scheduleTimezoneHint")}
|
|
701
797
|
</span>
|
|
702
798
|
<EntryPicker
|
|
@@ -721,18 +817,39 @@ function ScheduleDialog({ close, add }: { close(): void; add(schedule: AgentSche
|
|
|
721
817
|
);
|
|
722
818
|
}
|
|
723
819
|
|
|
724
|
-
|
|
725
|
-
|
|
820
|
+
/**
|
|
821
|
+
* Which model does the thinking, what it costs per million tokens — and what this agent has
|
|
822
|
+
* actually cost with it.
|
|
823
|
+
*
|
|
824
|
+
* ⚠️ Changing it writes a new definition version like every other edit here; the runtime picks the
|
|
825
|
+
* provider up on its next read (its definition cache is 60 seconds).
|
|
826
|
+
*
|
|
827
|
+
* ⚠️ The figures come from intel's catalog since #257 and no longer from a table in this bundle.
|
|
828
|
+
* The table was a copy that aged silently, and the reason it stayed one — "a token does not belong
|
|
829
|
+
* in a SPA" — was correct and is exactly why the fetch moved to intel rather than into this
|
|
830
|
+
* component.
|
|
831
|
+
*
|
|
832
|
+
* ⚠️ The two numbers beside each other are a price and a bill, and they must not read as one thing.
|
|
833
|
+
* A price per million tokens answers "what does this model cost"; the thirty-day figure answers
|
|
834
|
+
* "what has this agent cost", it is money already spent on runs that used whatever model was chosen
|
|
835
|
+
* AT THE TIME, and the sentence under it says so. Without that, switching the model would make the
|
|
836
|
+
* old figure read as a forecast for the new one.
|
|
837
|
+
*/
|
|
726
838
|
function ModelSection({
|
|
727
839
|
definition,
|
|
728
840
|
agent,
|
|
841
|
+
agentId,
|
|
729
842
|
}: {
|
|
730
843
|
definition: AgentDefinition;
|
|
731
844
|
agent: AgentDefinitionHandle;
|
|
845
|
+
agentId: string;
|
|
732
846
|
}) {
|
|
733
847
|
const i18n = useI18n();
|
|
848
|
+
const catalog = useModelCatalog();
|
|
849
|
+
const costs = useAgentCosts(agentId);
|
|
734
850
|
const models = selectableModels(definition.model);
|
|
735
|
-
const current = modelFacts(definition.model);
|
|
851
|
+
const current = modelFacts(definition.model, catalog.data?.entries);
|
|
852
|
+
const spent = costWindow(costs.data, 30);
|
|
736
853
|
|
|
737
854
|
return (
|
|
738
855
|
<Section title={i18n.t("agent.model")} hint={i18n.t("agent.modelHint")}>
|
|
@@ -753,7 +870,7 @@ function ModelSection({
|
|
|
753
870
|
</SelectTrigger>
|
|
754
871
|
<SelectContent>
|
|
755
872
|
{models.map((model) => {
|
|
756
|
-
const facts = modelFacts(model);
|
|
873
|
+
const facts = modelFacts(model, catalog.data?.entries);
|
|
757
874
|
return (
|
|
758
875
|
<SelectItem key={modelKey(model)} value={modelKey(model)}>
|
|
759
876
|
{/* ⚠️ The space is not formatting. The option's accessible name is its text run
|
|
@@ -767,10 +884,79 @@ function ModelSection({
|
|
|
767
884
|
</Select>
|
|
768
885
|
<ModelFigures facts={current} />
|
|
769
886
|
</div>
|
|
887
|
+
{/* ⚠️ Said once, under the row, and only when something on screen actually came off the
|
|
888
|
+
table. A silent fallback to typed-out prices is the state this feature replaced. */}
|
|
889
|
+
{current.stale ? (
|
|
890
|
+
<p className="mt-2 text-xs text-muted-foreground">{i18n.t("agent.modelFiguresStale")}</p>
|
|
891
|
+
) : null}
|
|
892
|
+
<AgentSpend
|
|
893
|
+
spent={spent}
|
|
894
|
+
status={costs.data?.status}
|
|
895
|
+
partial={costs.data?.partial ?? false}
|
|
896
|
+
/>
|
|
770
897
|
</Section>
|
|
771
898
|
);
|
|
772
899
|
}
|
|
773
900
|
|
|
901
|
+
/**
|
|
902
|
+
* What this agent has actually cost in the last thirty days (#257).
|
|
903
|
+
*
|
|
904
|
+
* ⚠️ Written in the past tense and labelled with the models that produced it, because it is the
|
|
905
|
+
* only thing standing between "$0.23" and a reader taking it for what the model above will cost.
|
|
906
|
+
* The models are named from the gateway's own log lines, so a period spent on a different model
|
|
907
|
+
* says so by itself.
|
|
908
|
+
*
|
|
909
|
+
* ⚠️ Nothing is rendered where the figure is not known. The Runs tab is where the reason belongs —
|
|
910
|
+
* it is the screen about runs — and repeating "no read token" beside a model select would be an
|
|
911
|
+
* error message in the middle of an unrelated decision.
|
|
912
|
+
*
|
|
913
|
+
* ⚠️ `partial` is the one exception to that division, and it has to be repeated here. It does not
|
|
914
|
+
* say the figure is missing — it says the figure shown IS NOT THE TOTAL, because the gateway read
|
|
915
|
+
* stopped at its page limit. A lower bound printed as if it were a sum is the same lie as a zero
|
|
916
|
+
* printed as if it were free, and this is where somebody reads it while deciding what to spend.
|
|
917
|
+
* An agent on a frequent schedule crosses that limit within thirty days as a matter of course; the
|
|
918
|
+
* seven-day figure practically never does, which makes the case rarer and therefore easier to miss.
|
|
919
|
+
*/
|
|
920
|
+
function AgentSpend({
|
|
921
|
+
spent,
|
|
922
|
+
status,
|
|
923
|
+
partial,
|
|
924
|
+
}: {
|
|
925
|
+
spent: AgentCostWindow | null;
|
|
926
|
+
status: AgentCosts["status"] | undefined;
|
|
927
|
+
partial: boolean;
|
|
928
|
+
}) {
|
|
929
|
+
const i18n = useI18n();
|
|
930
|
+
if (status !== "read" || !spent) return null;
|
|
931
|
+
|
|
932
|
+
return (
|
|
933
|
+
<p className="mt-3 text-sm">
|
|
934
|
+
<span className="font-medium">
|
|
935
|
+
{i18n.t("agent.costsSpent", {
|
|
936
|
+
amount: formatCost(spent.cost, i18n.locale),
|
|
937
|
+
days: String(spent.days),
|
|
938
|
+
})}
|
|
939
|
+
</span>
|
|
940
|
+
{spent.models.length > 0 ? (
|
|
941
|
+
<span className="text-muted-foreground">
|
|
942
|
+
{" "}
|
|
943
|
+
{i18n.t("agent.costsSpentWith", { models: spent.models.join(", ") })}
|
|
944
|
+
</span>
|
|
945
|
+
) : null}
|
|
946
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
947
|
+
{i18n.t("agent.costsSpentPast")}
|
|
948
|
+
</span>
|
|
949
|
+
{/* The Runs tab's own wording, deliberately the same key: two different sentences for one
|
|
950
|
+
fact would leave a reader wondering whether they are the same fact. */}
|
|
951
|
+
{partial ? (
|
|
952
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
953
|
+
{i18n.t("agent.costPartial")}
|
|
954
|
+
</span>
|
|
955
|
+
) : null}
|
|
956
|
+
</p>
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
774
960
|
// Brand first, then the model's own name — "Claude Sonnet 5", not "claude-sonnet-5 · anthropic". The
|
|
775
961
|
// provider stays in the definition; it names who serves the model, which is not what anybody reads a
|
|
776
962
|
// list of models for.
|
|
@@ -60,6 +60,23 @@ export function agentRuntimeMissing(error: unknown): boolean {
|
|
|
60
60
|
* deployment's master secret was replaced — but the reader's move is identical, and a second
|
|
61
61
|
* sentence saying the same thing differently would only be a second thing to keep in step.
|
|
62
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
|
+
|
|
63
80
|
export function agentKeyMissing(error: unknown): boolean {
|
|
64
81
|
return (
|
|
65
82
|
error instanceof IntelRequestError &&
|