@anchrd/intel-ui 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +2 -9
  2. package/src/app/app-tree/app-tree.tsx +59 -147
  3. package/src/app/settings-dialog/settings-dialog.tsx +10 -32
  4. package/src/data/intel-data-provider/intel-data-provider.ts +0 -163
  5. package/src/data/intel-data-provider/intel-data-provider.types.ts +0 -100
  6. package/src/entry-picker/entry-picker.tsx +6 -6
  7. package/src/i18n/de.json +6 -213
  8. package/src/i18n/en.json +6 -213
  9. package/src/i18n/es.json +6 -213
  10. package/src/import-bundle/import-bundle.tsx +152 -0
  11. package/src/kind-icon.ts +1 -15
  12. package/src/main.tsx +7 -10
  13. package/src/node-table/node-table.tsx +5 -4
  14. package/src/nodes/nodes.tsx +0 -34
  15. package/src/resource-menu/resource-menu.tsx +68 -20
  16. package/src/section-hint/section-hint.tsx +2 -2
  17. package/src/title-row/title-row.tsx +5 -22
  18. package/src/tools/tools.tsx +0 -2
  19. package/vite.config.ts +0 -4
  20. package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
  21. package/src/agent/agent-calendar/agent-calendar.tsx +0 -135
  22. package/src/agent/agent-chat/agent-chat.tsx +0 -118
  23. package/src/agent/agent-costs/agent-costs.ts +0 -95
  24. package/src/agent/agent-cron/agent-cron.ts +0 -68
  25. package/src/agent/agent-definition/agent-definition.ts +0 -96
  26. package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
  27. package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
  28. package/src/agent/agent-log/agent-log.tsx +0 -308
  29. package/src/agent/agent-models/agent-models.ts +0 -166
  30. package/src/agent/agent-models/agent-models.types.ts +0 -24
  31. package/src/agent/agent-profile/agent-profile.tsx +0 -988
  32. package/src/agent/agent-state/agent-state.ts +0 -85
  33. package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
  34. package/src/agent/agent.tsx +0 -482
  35. package/src/board/board-calendar/board-calendar.tsx +0 -89
  36. package/src/board/board-card/board-card.tsx +0 -106
  37. package/src/board/board-data/board-data.ts +0 -241
  38. package/src/board/board-data/board-data.types.ts +0 -63
  39. package/src/board/board-detail/board-detail.tsx +0 -629
  40. package/src/board/board-gantt/board-gantt.ts +0 -545
  41. package/src/board/board-gantt/board-gantt.tsx +0 -286
  42. package/src/board/board-graph/board-graph.ts +0 -174
  43. package/src/board/board-graph/board-graph.tsx +0 -168
  44. package/src/board/board-items/board-items.ts +0 -183
  45. package/src/board/board-kanban/board-kanban.ts +0 -97
  46. package/src/board/board-kanban/board-kanban.tsx +0 -211
  47. package/src/board/board-status/board-status.ts +0 -59
  48. package/src/board/board-statuses/board-statuses.ts +0 -63
  49. package/src/board/board-statuses/board-statuses.tsx +0 -228
  50. package/src/board/board-table/board-table.ts +0 -33
  51. package/src/board/board-table/board-table.tsx +0 -413
  52. package/src/board/board-views/board-views.tsx +0 -68
  53. package/src/board/board-views/board-views.types.ts +0 -29
  54. package/src/board/board.tsx +0 -251
  55. package/src/components/ui/item-calendar.tsx +0 -181
  56. package/src/components/ui/item-gantt.tsx +0 -463
  57. package/src/components/ui/kanban.tsx +0 -245
  58. package/src/data/agent-runtime/agent-runtime.ts +0 -131
  59. package/src/hooks/use-capabilities.ts +0 -22
  60. package/src/hooks/use-model-catalog.ts +0 -26
  61. package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
  62. package/src/timezone/timezone-context.tsx +0 -65
  63. package/src/timezone/timezone.ts +0 -174
@@ -1,308 +0,0 @@
1
- import type { AgentCosts } from "@anchrd/intel-contract";
2
- import { useQuery } from "@tanstack/react-query";
3
- import { ChevronDown, ChevronRight } from "lucide-react";
4
- import { useState } from "react";
5
- import {
6
- formatCost,
7
- formatTokens,
8
- type RunCost,
9
- runCost,
10
- runUsage,
11
- useAgentCosts,
12
- } from "@/agent/agent-costs/agent-costs.ts";
13
- import type { AgentRun } from "@/data/agent-runtime/agent-runtime.ts";
14
- import type { I18n } from "@/i18n/i18n.types.ts";
15
- import { useI18n } from "@/i18n/i18n-context.tsx";
16
- import { useIntelRouterContext } from "@/router/router-context.ts";
17
-
18
- /**
19
- * What this agent has done, newest first.
20
- *
21
- * ⚠️ Read from the RUNTIME, not from intel: runs live in the agent's own Durable Object and are
22
- * trimmed there. So this list is short by design and has no paging — the runtime answers at most a
23
- * hundred runs and keeps no more.
24
- *
25
- * ⚠️ A run says what it did, when it failed what stopped it, and since #268 what it answered — but
26
- * only for the runs nobody was watching (`schedule`, `manual`, `task`). A chat answer belongs to the
27
- * person it was streamed to and a mail answer to the correspondent, and the runtime deliberately
28
- * stores neither where this list could reach it.
29
- *
30
- * ⚠️ Since #251 it also says what each run consumed and what it cost — and the two come from
31
- * DIFFERENT places on purpose. The tokens are the runtime's own record; the money is Cloudflare's
32
- * gateway log, read by intel. Either can be missing without the other, and each says so on its own
33
- * rather than being folded into one number that would be half a guess.
34
- */
35
- export function AgentLog({ agentId }: { agentId: string }) {
36
- const { data } = useIntelRouterContext();
37
- const i18n = useI18n();
38
- const [openRunId, setOpenRunId] = useState<string | null>(null);
39
- const runs = useQuery({
40
- queryKey: ["agent-runs", agentId],
41
- queryFn: () => data.listAgentRuns(agentId),
42
- });
43
- const costs = useAgentCosts(agentId);
44
-
45
- return (
46
- <section aria-label={i18n.t("agent.log")} className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
47
- {runs.isPending ? (
48
- <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
49
- ) : null}
50
- {runs.isError ? (
51
- <div role="alert" className="space-y-3 text-sm">
52
- <p className="text-destructive">{i18n.t("agent.logFailed")}</p>
53
- <button
54
- type="button"
55
- onClick={() => void runs.refetch()}
56
- className="rounded-md border px-3 py-2 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
57
- >
58
- {i18n.t("common.retry")}
59
- </button>
60
- </div>
61
- ) : null}
62
- {runs.data && runs.data.items.length === 0 ? (
63
- <p className="text-sm text-muted-foreground">{i18n.t("agent.logEmpty")}</p>
64
- ) : null}
65
- {runs.data && runs.data.items.length > 0 ? (
66
- <>
67
- <CostSummary costs={costs.data} />
68
- <ul className="space-y-2">
69
- {runs.data.items.map((run) => (
70
- <RunRow
71
- key={run.id}
72
- agentId={agentId}
73
- run={run}
74
- cost={runCost(costs.data, run.id)}
75
- open={openRunId === run.id}
76
- toggle={() => setOpenRunId(openRunId === run.id ? null : run.id)}
77
- />
78
- ))}
79
- </ul>
80
- </>
81
- ) : null}
82
- </section>
83
- );
84
- }
85
-
86
- /**
87
- * What the agent has cost over the two windows, above the list it belongs to.
88
- *
89
- * ⚠️ Where the figures are missing this says WHY, in a sentence, instead of showing nothing. A cost
90
- * view that stayed blank when the gateway was unreachable looks exactly like an agent that has cost
91
- * nothing — which is the reading this whole feature exists to prevent.
92
- *
93
- * ⚠️ `partial` is spelled out rather than hidden behind a symbol. A total that stopped counting is
94
- * not a total, and a reader comparing this month with last needs to know the number is a floor.
95
- */
96
- function CostSummary({ costs }: { costs: AgentCosts | undefined }) {
97
- const i18n = useI18n();
98
- if (!costs) return null;
99
- if (costs.status !== "read") {
100
- return (
101
- <p className="mb-3 text-sm text-muted-foreground">
102
- {i18n.t(
103
- costs.status === "not_configured" ? "agent.costNotConfigured" : "agent.costUnreadable",
104
- )}
105
- </p>
106
- );
107
- }
108
-
109
- return (
110
- <div className="mb-3 space-y-1">
111
- <dl className="flex flex-wrap items-baseline gap-x-6 gap-y-1 text-sm">
112
- {costs.windows.map((window) => (
113
- <div key={window.days} className="flex items-baseline gap-2">
114
- <dt className="text-muted-foreground">
115
- {i18n.t("agent.costWindow", { days: String(window.days) })}
116
- </dt>
117
- <dd className="font-medium">{formatCost(window.cost, i18n.locale)}</dd>
118
- <dd className="text-xs text-muted-foreground">
119
- {i18n.t("agent.costWindowCalls", { count: String(window.calls) })}
120
- </dd>
121
- </div>
122
- ))}
123
- </dl>
124
- {costs.partial ? (
125
- <p className="text-xs text-muted-foreground">{i18n.t("agent.costPartial")}</p>
126
- ) : null}
127
- </div>
128
- );
129
- }
130
-
131
- /**
132
- * What one run consumed, in the four counters that are priced differently.
133
- *
134
- * ⚠️ Cache reads stand APART from fresh input and are never added into one number. They are the
135
- * figure #250 is visible in — a read costs about a tenth of an ordinary input token — and in a
136
- * total they disappear completely.
137
- */
138
- function RunUsage({ run, i18n }: { run: AgentRun; i18n: I18n }) {
139
- const usage = runUsage(run);
140
- if (!usage) {
141
- // ⚠️ Two different sentences, and the difference is worth the extra key. "Nothing recorded" is a
142
- // run from before #250; "the provider reports nothing" is every Workers AI run, for good.
143
- return (
144
- <span className="text-muted-foreground">
145
- {i18n.t(run.usage ? "agent.usageNotReported" : "agent.usageUnknown")}
146
- </span>
147
- );
148
- }
149
- return (
150
- <span className="text-muted-foreground">
151
- {i18n.t("agent.usage", {
152
- input: formatTokens(usage.inputTokens, i18n.locale),
153
- output: formatTokens(usage.outputTokens, i18n.locale),
154
- cacheRead: formatTokens(usage.cacheReadInputTokens, i18n.locale),
155
- cacheWrite: formatTokens(usage.cacheCreationInputTokens, i18n.locale),
156
- })}
157
- </span>
158
- );
159
- }
160
-
161
- /**
162
- * ⚠️ Four outcomes, one of which is a number. A run the gateway has no line for says so; it does
163
- * NOT show a zero, and it does not silently disappear from a column its neighbours fill.
164
- */
165
- function RunCostLabel({ cost, i18n }: { cost: RunCost; i18n: I18n }) {
166
- if (cost.kind === "known") {
167
- return <span className="font-medium">{formatCost(cost.cost, i18n.locale)}</span>;
168
- }
169
- // The two deployment-wide reasons are already stated once above the list, so a row under them
170
- // stays quiet rather than repeating the same sentence for every run on the screen.
171
- if (cost.kind !== "missing") return null;
172
- return <span className="text-muted-foreground">{i18n.t("agent.costMissing")}</span>;
173
- }
174
-
175
- function RunRow({
176
- agentId,
177
- run,
178
- cost,
179
- open,
180
- toggle,
181
- }: {
182
- agentId: string;
183
- run: AgentRun;
184
- cost: RunCost;
185
- open: boolean;
186
- toggle(): void;
187
- }) {
188
- const i18n = useI18n();
189
- const failed = run.status === "failed";
190
- const Chevron = open ? ChevronDown : ChevronRight;
191
- return (
192
- <li className="rounded-lg border bg-card">
193
- <button
194
- type="button"
195
- onClick={toggle}
196
- aria-expanded={open}
197
- className="flex w-full items-start gap-3 rounded-lg px-4 py-3 text-left outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
198
- >
199
- <Chevron aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
200
- <span className="min-w-0 flex-1">
201
- <span className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
202
- <span
203
- className={`rounded-full border px-2 py-0.5 text-xs ${failed ? "border-destructive/40 text-destructive" : "text-muted-foreground"}`}
204
- >
205
- {i18n.t(`runs.status.${run.status}`)}
206
- </span>
207
- <time dateTime={run.startedAt}>
208
- {new Date(run.startedAt).toLocaleString(i18n.locale)}
209
- </time>
210
- <span className="text-muted-foreground">{i18n.t(`agent.trigger.${run.trigger}`)}</span>
211
- <span className="text-muted-foreground">
212
- {i18n.t("agent.steps", { count: run.steps })}
213
- </span>
214
- <RunCostLabel cost={cost} i18n={i18n} />
215
- </span>
216
- <span className="mt-1 block text-xs">
217
- <RunUsage run={run} i18n={i18n} />
218
- </span>
219
- {/* The failure in the words the failure itself used. Summarising it into "failed" is what
220
- the status pill already says, and it is never the sentence that helps. */}
221
- {run.error ? (
222
- <span className="mt-1.5 block text-sm text-destructive">{run.error}</span>
223
- ) : null}
224
- </span>
225
- </button>
226
- {open ? <RunDetail agentId={agentId} run={run} /> : null}
227
- </li>
228
- );
229
- }
230
-
231
- /**
232
- * What an unattended run produced, which is the only place a person can read it (#272).
233
- *
234
- * ⚠️ Rendered for `schedule`, `manual` and `task`, and for nothing else. A `chat` or `mail` run
235
- * carries no answer by construction (#268), so an empty state under one of those would invent a
236
- * loss: the answer was streamed into a window or posted to a correspondent and was never meant to
237
- * come back here.
238
- *
239
- * ⚠️ Read from the run the row already holds, not from the detail fetch. `GET /runs` carries the
240
- * whole record, and hanging the answer off the second request would hide it whenever the
241
- * operational log — the smaller half of what this row is for — happens to be unreadable.
242
- */
243
- function RunAnswer({ run, i18n }: { run: AgentRun; i18n: I18n }) {
244
- if (run.trigger === "chat" || run.trigger === "mail") return null;
245
- return (
246
- <section aria-label={i18n.t("agent.runAnswer")} className="mb-3">
247
- <h3 className="text-sm font-semibold">{i18n.t("agent.runAnswer")}</h3>
248
- {run.answer ? (
249
- // Whitespace preserved: a model answers in paragraphs and lists, and collapsing them turns
250
- // a readable report into one block of prose.
251
- <p className="mt-1 whitespace-pre-wrap text-sm">{run.answer}</p>
252
- ) : (
253
- <p className="mt-1 text-sm text-muted-foreground">{i18n.t("agent.runAnswerEmpty")}</p>
254
- )}
255
- </section>
256
- );
257
- }
258
-
259
- // The operational log of one run: what it tried, in order. Fetched only when a row is opened —
260
- // every run carries its own entries and loading all of them would be the whole log for one screen.
261
- function RunDetail({ agentId, run }: { agentId: string; run: AgentRun }) {
262
- const { data } = useIntelRouterContext();
263
- const i18n = useI18n();
264
- const runId = run.id;
265
- const detail = useQuery({
266
- queryKey: ["agent-run", agentId, runId],
267
- queryFn: () => data.getAgentRun(agentId, runId),
268
- });
269
- return (
270
- <div className="border-t px-4 py-3">
271
- <RunAnswer run={run} i18n={i18n} />
272
- {detail.isPending ? (
273
- <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
274
- ) : null}
275
- {detail.isError ? (
276
- <p role="alert" className="text-sm text-destructive">
277
- {i18n.t("agent.runFailed")}
278
- </p>
279
- ) : null}
280
- {detail.data && detail.data.log.length === 0 ? (
281
- <p className="text-sm text-muted-foreground">{i18n.t("agent.runLogEmpty")}</p>
282
- ) : null}
283
- {detail.data && detail.data.log.length > 0 ? (
284
- <ol aria-label={i18n.t("agent.runLog")} className="space-y-2">
285
- {detail.data.log.map((entry) => (
286
- <li key={entry.id} className="text-sm">
287
- <span
288
- className={entry.level === "error" ? "font-medium text-destructive" : "font-medium"}
289
- >
290
- {entry.event}
291
- </span>
292
- <time dateTime={entry.at} className="ml-2 text-xs text-muted-foreground">
293
- {new Date(entry.at).toLocaleTimeString(i18n.locale)}
294
- </time>
295
- {entry.detail ? (
296
- <span
297
- className={`mt-0.5 block text-xs ${entry.level === "error" ? "text-destructive" : "text-muted-foreground"}`}
298
- >
299
- {entry.detail}
300
- </span>
301
- ) : null}
302
- </li>
303
- ))}
304
- </ol>
305
- ) : null}
306
- </div>
307
- );
308
- }
@@ -1,166 +0,0 @@
1
- import { AgentModel, type ModelCatalogEntry } from "@anchrd/intel-contract";
2
- import { z } from "zod";
3
- import type { ModelFacts } from "./agent-models.types.ts";
4
-
5
- /**
6
- * Which models this installation offers.
7
- *
8
- * ⚠️ There is no endpoint to ask. The runtime's registry answers one question — "is this provider
9
- * configured" — and only when a run is already starting; nothing anywhere lists what a deployment
10
- * pays for. Rather than invent an API for one screen, the list is build configuration
11
- * (`VITE_AGENT_MODELS`, a JSON array of `{provider, model}`), with the offering below as the default.
12
- *
13
- * ⚠️ The definition's current model is always offered, even when it is not in the list. A select
14
- * that silently dropped it would turn "look at this agent" into "change this agent" for anyone who
15
- * then saved — and the definition is written by MCP and by other installations too, not only here.
16
- */
17
- const ConfiguredModels = z.array(AgentModel);
18
-
19
- /**
20
- * ⚠️ The brand comes from the model id, never from `provider`. `provider` says who serves the model,
21
- * so every Workers AI entry would read "Cloudflare" — thirteen identical labels in a list whose whole
22
- * job is to tell them apart. The name a reader looks for sits in the id, as its second segment:
23
- * `@cf/openai/gpt-oss-120b` is OpenAI's.
24
- */
25
- const brands: Record<string, string> = {
26
- aisingapore: "AI Singapore",
27
- "deepseek-ai": "DeepSeek",
28
- google: "Google",
29
- "ibm-granite": "IBM",
30
- meta: "Meta",
31
- "meta-llama": "Meta",
32
- mistralai: "Mistral",
33
- moonshotai: "Moonshot",
34
- nvidia: "NVIDIA",
35
- openai: "OpenAI",
36
- qwen: "Qwen",
37
- "zai-org": "Z.ai",
38
- };
39
-
40
- const firstFallback: AgentModel = { provider: "anthropic", model: "claude-sonnet-5" };
41
- const fallback: [AgentModel, ...AgentModel[]] = [
42
- firstFallback,
43
- { provider: "anthropic", model: "claude-opus-5" },
44
- { provider: "anthropic", model: "claude-haiku-4-5" },
45
- { provider: "anthropic", model: "claude-sonnet-4" },
46
- { provider: "workers-ai", model: "@cf/openai/gpt-oss-120b" },
47
- { provider: "workers-ai", model: "@cf/openai/gpt-oss-20b" },
48
- { provider: "workers-ai", model: "@cf/moonshotai/kimi-k2.6" },
49
- { provider: "workers-ai", model: "@cf/moonshotai/kimi-k2.7-code" },
50
- { provider: "workers-ai", model: "@cf/zai-org/glm-5.2" },
51
- { provider: "workers-ai", model: "@cf/zai-org/glm-4.7-flash" },
52
- { provider: "workers-ai", model: "@cf/google/gemma-4-26b-a4b-it" },
53
- { provider: "workers-ai", model: "@cf/nvidia/nemotron-3-120b-a12b" },
54
- { provider: "workers-ai", model: "@cf/meta/llama-4-scout-17b-16e-instruct" },
55
- { provider: "workers-ai", model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast" },
56
- { provider: "workers-ai", model: "@cf/mistralai/mistral-small-3.1-24b-instruct" },
57
- { provider: "workers-ai", model: "@cf/ibm-granite/granite-4.0-h-micro" },
58
- { provider: "workers-ai", model: "@cf/qwen/qwen3-30b-a3b-fp8" },
59
- ];
60
-
61
- export function modelKey(model: AgentModel): string {
62
- return `${model.provider}:${model.model}`;
63
- }
64
-
65
- export function parseModelKey(key: string): AgentModel | null {
66
- const boundary = key.indexOf(":");
67
- if (boundary <= 0) return null;
68
- const parsed = AgentModel.safeParse({
69
- provider: key.slice(0, boundary),
70
- model: key.slice(boundary + 1),
71
- });
72
- return parsed.success ? parsed.data : null;
73
- }
74
-
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
- );
94
- if (known) {
95
- return {
96
- brand: brandOf(model),
97
- name: known.name,
98
- contextTokens: known.contextTokens,
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,
103
- };
104
- }
105
- return {
106
- brand: brandOf(model),
107
- name: unknownName(model),
108
- contextTokens: null,
109
- price: null,
110
- stale: false,
111
- };
112
- }
113
-
114
- function brandOf(model: AgentModel): string {
115
- if (model.provider === "anthropic") return "Claude";
116
- const vendor = model.model.split("/")[1];
117
- if (!vendor) return "Workers AI";
118
- return brands[vendor] ?? vendor;
119
- }
120
-
121
- // The id itself, not a prettified guess. "claude-opus-4-1" would become "Opus 4 1" under any rule
122
- // simple enough to write here, and a wrong name is harder to recognise than a raw one.
123
- function unknownName(model: AgentModel): string {
124
- if (model.provider === "anthropic") return model.model.replace(/^claude-/, "");
125
- const segments = model.model.split("/");
126
- return segments[segments.length - 1] ?? model.model;
127
- }
128
-
129
- export function formatContextTokens(tokens: number): string {
130
- if (tokens >= 1_000_000) return `${Math.round(tokens / 1_000_000)}M`;
131
- if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`;
132
- return String(tokens);
133
- }
134
-
135
- // ⚠️ Two decimals at least, three at most, and always per one million tokens. Granite costs $0.017
136
- // per million in: rounded to two decimals it reads $0.02, which is the same price as a model that
137
- // costs seventeen percent more.
138
- export function formatPricePerMillion(dollars: number, locale: string): string {
139
- return dollars.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 3 });
140
- }
141
-
142
- export function configuredModels(): AgentModel[] {
143
- const raw = import.meta.env.VITE_AGENT_MODELS;
144
- if (!raw) return fallback;
145
- try {
146
- const parsed = ConfiguredModels.safeParse(JSON.parse(raw));
147
- // A malformed variable falls back rather than emptying the select: an agent whose model cannot
148
- // be chosen is an agent that cannot be repaired from this screen.
149
- return parsed.success && parsed.data.length > 0 ? parsed.data : fallback;
150
- } catch {
151
- return fallback;
152
- }
153
- }
154
-
155
- // The offered list with the agent's own model folded in, deduplicated, order preserved.
156
- export function selectableModels(current: AgentModel | null): AgentModel[] {
157
- const models = configuredModels();
158
- if (!current || models.some((model) => modelKey(model) === modelKey(current))) return models;
159
- return [current, ...models];
160
- }
161
-
162
- // What a newly created agent gets. The first offered model, because a create dialog that asked for
163
- // a provider before the agent has a name would be a settings screen wearing a form's clothes.
164
- export function defaultModel(): AgentModel {
165
- return configuredModels()[0] ?? firstFallback;
166
- }
@@ -1,24 +0,0 @@
1
- import type { ModelPrice } from "@anchrd/intel-contract";
2
-
3
- /** What a reader needs in order to pick a model: whose it is, what it is called, and what it costs. */
4
- export interface ModelFacts {
5
- brand: string;
6
- name: string;
7
- /**
8
- * ⚠️ Both are null for a model this installation has no figures for, and the display then shows
9
- * nothing rather than a zero. A definition is written by MCP and by other installations too, so an
10
- * unknown model is normal — and an invented "0" would be a false statement about money.
11
- */
12
- contextTokens: number | null;
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;
22
- }
23
-
24
- export type { ModelPrice };