@anchrd/intel-ui 0.14.0 → 0.15.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/src/i18n/en.json CHANGED
@@ -282,12 +282,13 @@
282
282
  "agent.tab.chat": "Chat",
283
283
  "agent.tab.calendar": "Calendar",
284
284
  "agent.tab.log": "Log",
285
- "agent.statusRunning": "Running",
286
- "agent.statusIdle": "Idle",
287
- "agent.statusPaused": "Paused",
288
- "agent.statusUnknown": "Runtime not reachable",
285
+ "agent.status.running": "Running",
286
+ "agent.status.waiting": "Waiting for its next run",
287
+ "agent.status.failed": "Last run failed",
288
+ "agent.status.paused": "Paused",
289
+ "agent.status.unknown": "Runtime not reachable",
289
290
  "agent.nextRun": "next run {when} ({zone})",
290
- "agent.noNextRun": "no scheduled run",
291
+ "agent.lastRun": "last run {when}",
291
292
  "agent.pausedNoRuns": "no schedule fires, no task and no mail is taken; the chat still answers",
292
293
  "agent.pause": "Pause",
293
294
  "agent.resume": "Resume",
@@ -296,6 +297,7 @@
296
297
  "agent.runNowPaused": "Resume the agent before running it.",
297
298
  "agent.actionFailed": "That did not work. Try again.",
298
299
  "agent.keyMissing": "This agent has no application key, so no run can start. Replace its key under Identity in the profile.",
300
+ "agent.createdBy": "by {name}",
299
301
  "agent.notPermitted": "You may read this agent, but not drive it. Driving an agent needs the “run agents” permission.",
300
302
  "agent.noRuntime": "This installation runs no agent runtime, so this agent cannot chat or run here. Its definition is kept and works again on an installation that has one.",
301
303
  "agent.loadFailed": "This agent could not be loaded. Check your access and try again.",
@@ -320,10 +322,10 @@
320
322
  "agent.contactAdd": "Add contact",
321
323
  "agent.contactAddReason": "A mail address belongs to the installation, not to the agent's definition, so it is configured where the runtime is deployed.",
322
324
  "agent.knows": "What it knows",
323
- "agent.knowsHint": "The folders this agent reads, and what each one counts as. A system message instructs it, semantic context is searched, memory is written back to.",
324
- "agent.addReference": "Add folder",
325
+ "agent.knowsHint": "What this agent reads, and what each entry counts as. A system message instructs it — a folder, a document or a table. Semantic context is searched and memory is written back to, so both of those are folders.",
326
+ "agent.addReference": "Add something to read",
325
327
  "agent.removeReferenceOf": "Stop reading {title}",
326
- "agent.pickFolder": "Folder to read",
328
+ "agent.pickEntry": "What to read",
327
329
  "agent.roleFor": "What {title} counts as",
328
330
  "agent.role.system-message": "System message",
329
331
  "agent.role.semantic-context": "Semantic context",
@@ -335,11 +337,12 @@
335
337
  "agent.removeToolServerOf": "Take {name} away from this agent",
336
338
  "agent.toolServerUnavailable": "You no longer reach this server",
337
339
  "agent.toolsDelegationNotice": "Anyone who may run this agent acts on your connection.",
340
+ "agent.toolsDelegationNoticeBy": "Anyone who may run this agent acts on the connection of {name}.",
341
+ "agent.toolsDelegationNoticeSomebody": "Anyone who may run this agent acts on the connection of whoever gave it these tools — not on yours.",
338
342
  "agent.toolsNotConnected": "You are not signed in to the company portal yet, so there is nothing to give away. Open Tools to connect.",
339
343
  "agent.toolsNoneLeft": "Every server you reach is already given to this agent.",
340
344
  "agent.schedule": "Schedule",
341
345
  "agent.scheduleHint": "When this agent acts on its own, and what it runs. Each schedule keeps the timezone it was given, so it means the same hour to everybody reading it.",
342
- "agent.scheduleEmpty": "This agent runs only when it is asked.",
343
346
  "agent.addSchedule": "Add schedule",
344
347
  "agent.removeSchedule": "Remove this schedule",
345
348
  "agent.scheduleTarget": "What to run",
@@ -350,8 +353,23 @@
350
353
  "agent.model": "Model",
351
354
  "agent.modelHint": "Which model does the thinking. A change is picked up by the runtime within a minute.",
352
355
  "agent.modelFigures": "{context} context · ${input} in / ${output} out per 1M tokens",
356
+ "agent.usage": "{input} in · {output} out · {cacheRead} cache read · {cacheWrite} cache written",
357
+ "agent.usageUnknown": "No usage figures for this run",
358
+ "agent.usageNotReported": "This provider reports no usage",
359
+ "agent.costMissing": "Cost not in the gateway log",
360
+ "agent.costNotConfigured": "No cost figures: this installation has no AI Gateway read token.",
361
+ "agent.costUnreadable": "No cost figures: the AI Gateway log could not be read.",
362
+ "agent.costPartial": "The gateway log was read up to its page limit, so these totals are a lower bound.",
363
+ "agent.costWindow": "Last {days} days",
364
+ "agent.costWindowCalls": "{count} model calls",
365
+ "agent.costs": "Cost",
366
+ "agent.costsSpent": "{amount} in the last {days} days",
367
+ "agent.costsSpentPast": "Already spent, on past runs — not a forecast for the model chosen above.",
368
+ "agent.costsSpentWith": "with {models}",
369
+ "agent.modelFiguresStale": "These figures come from a built-in table and can be out of date.",
353
370
  "agent.calendar": "Upcoming runs",
354
- "agent.calendarZones": "Each time is shown in the timezone of its own schedule, because that is when the agent runs.",
371
+ "agent.calendarZones": "Each time is shown in the timezone of its own schedule, because that is when the agent runs. Where your own timezone differs, it is added beside it.",
372
+ "agent.calendarYourTime": "{when} your time",
355
373
  "agent.calendarUnreadable": "None of this agent's schedules can be read as a cron expression, so nothing is planned.",
356
374
  "agent.log": "Runs",
357
375
  "agent.logEmpty": "This agent has not run yet.",
package/src/i18n/es.json CHANGED
@@ -282,12 +282,13 @@
282
282
  "agent.tab.chat": "Chat",
283
283
  "agent.tab.calendar": "Calendario",
284
284
  "agent.tab.log": "Registro",
285
- "agent.statusRunning": "En marcha",
286
- "agent.statusIdle": "Inactivo",
287
- "agent.statusPaused": "En pausa",
288
- "agent.statusUnknown": "Entorno de ejecución no accesible",
285
+ "agent.status.running": "En marcha",
286
+ "agent.status.waiting": "Esperando su próxima ejecución",
287
+ "agent.status.failed": "La última ejecución falló",
288
+ "agent.status.paused": "En pausa",
289
+ "agent.status.unknown": "Entorno de ejecución no accesible",
289
290
  "agent.nextRun": "próxima ejecución {when} ({zone})",
290
- "agent.noNextRun": "sin ejecución programada",
291
+ "agent.lastRun": "última ejecución {when}",
291
292
  "agent.pausedNoRuns": "no se dispara ningún horario, no se acepta ninguna tarea ni ningún correo; el chat sigue respondiendo",
292
293
  "agent.pause": "Pausar",
293
294
  "agent.resume": "Reanudar",
@@ -296,6 +297,7 @@
296
297
  "agent.runNowPaused": "Reanuda el agente antes de ejecutarlo.",
297
298
  "agent.actionFailed": "Eso no ha funcionado. Inténtalo de nuevo.",
298
299
  "agent.keyMissing": "Este agente no tiene clave de aplicación, así que no puede empezar ninguna ejecución. Sustituye su clave en «Identidad», dentro del perfil.",
300
+ "agent.createdBy": "por {name}",
299
301
  "agent.notPermitted": "Puedes leer este agente, pero no manejarlo. Manejar un agente necesita el permiso «ejecutar agentes».",
300
302
  "agent.noRuntime": "Esta instalación no tiene entorno de ejecución de agentes, así que este agente no puede chatear ni ejecutarse aquí. Su definición se conserva y vuelve a funcionar en una instalación que sí lo tenga.",
301
303
  "agent.loadFailed": "Este agente no se ha podido cargar. Comprueba tu acceso e inténtalo de nuevo.",
@@ -320,10 +322,10 @@
320
322
  "agent.contactAdd": "Añadir un contacto",
321
323
  "agent.contactAddReason": "Una dirección de correo pertenece a la instalación, no a la definición del agente, así que se configura donde está desplegado el entorno de ejecución.",
322
324
  "agent.knows": "Lo que sabe",
323
- "agent.knowsHint": "Las carpetas que lee este agente y para qué cuenta cada una. Un mensaje de sistema lo instruye, el contexto semántico se busca, en la memoria se escribe de vuelta.",
324
- "agent.addReference": "Añadir una carpeta",
325
+ "agent.knowsHint": "Lo que lee este agente y para qué cuenta cada entrada. Un mensaje de sistema lo instruye — una carpeta, un documento o una tabla. El contexto semántico se busca y en la memoria se escribe de vuelta, por eso ambos son carpetas.",
326
+ "agent.addReference": "Añadir algo para leer",
325
327
  "agent.removeReferenceOf": "Dejar de leer {title}",
326
- "agent.pickFolder": "Carpeta que leer",
328
+ "agent.pickEntry": "Qué se lee",
327
329
  "agent.roleFor": "Para qué cuenta {title}",
328
330
  "agent.role.system-message": "Mensaje de sistema",
329
331
  "agent.role.semantic-context": "Contexto semántico",
@@ -335,11 +337,12 @@
335
337
  "agent.removeToolServerOf": "Quitarle {name} a este agente",
336
338
  "agent.toolServerUnavailable": "Ya no alcanzas este servidor",
337
339
  "agent.toolsDelegationNotice": "Quien pueda ejecutar este agente actúa sobre tu conexión.",
340
+ "agent.toolsDelegationNoticeBy": "Quien pueda ejecutar este agente actúa sobre la conexión de {name}.",
341
+ "agent.toolsDelegationNoticeSomebody": "Quien pueda ejecutar este agente actúa sobre la conexión de quien le dio estas herramientas, no sobre la tuya.",
338
342
  "agent.toolsNotConnected": "Todavía no has iniciado sesión en el portal de la empresa, así que no hay nada que ceder. Abre «Herramientas» para conectarte.",
339
343
  "agent.toolsNoneLeft": "Todos los servidores que alcanzas ya están dados a este agente.",
340
344
  "agent.schedule": "Horario",
341
345
  "agent.scheduleHint": "Cuándo actúa este agente por su cuenta y qué ejecuta. Cada horario conserva la zona horaria con la que se creó, así que significa la misma hora para todo el que lo lea.",
342
- "agent.scheduleEmpty": "Este agente solo se ejecuta cuando se le pide.",
343
346
  "agent.addSchedule": "Añadir un horario",
344
347
  "agent.removeSchedule": "Quitar este horario",
345
348
  "agent.scheduleTarget": "Qué ejecutar",
@@ -350,8 +353,23 @@
350
353
  "agent.model": "Modelo",
351
354
  "agent.modelHint": "Qué modelo piensa. El entorno de ejecución recoge un cambio en menos de un minuto.",
352
355
  "agent.modelFigures": "{context} de contexto · {input} $ de entrada / {output} $ de salida por cada millón de tokens",
356
+ "agent.usage": "{input} de entrada · {output} de salida · {cacheRead} leídos de caché · {cacheWrite} escritos en caché",
357
+ "agent.usageUnknown": "No hay cifras de consumo para esta ejecución",
358
+ "agent.usageNotReported": "Este proveedor no informa del consumo",
359
+ "agent.costMissing": "El coste no figura en el registro de la pasarela",
360
+ "agent.costNotConfigured": "Sin cifras de coste: esta instalación no tiene un token de lectura de AI Gateway.",
361
+ "agent.costUnreadable": "Sin cifras de coste: no se pudo leer el registro de AI Gateway.",
362
+ "agent.costPartial": "El registro de la pasarela se leyó hasta su límite de páginas, así que estos totales son un mínimo.",
363
+ "agent.costWindow": "Últimos {days} días",
364
+ "agent.costWindowCalls": "{count} llamadas al modelo",
365
+ "agent.costs": "Coste",
366
+ "agent.costsSpent": "{amount} en los últimos {days} días",
367
+ "agent.costsSpentPast": "Ya gastado, en ejecuciones pasadas: no es una previsión para el modelo elegido arriba.",
368
+ "agent.costsSpentWith": "con {models}",
369
+ "agent.modelFiguresStale": "Estas cifras proceden de una tabla incorporada y pueden estar desactualizadas.",
353
370
  "agent.calendar": "Próximas ejecuciones",
354
- "agent.calendarZones": "Cada hora se muestra en la zona horaria de su propio horario, porque es cuando se ejecuta el agente.",
371
+ "agent.calendarZones": "Cada hora se muestra en la zona horaria de su propio horario, porque es cuando se ejecuta el agente. Si tu propia zona horaria difiere, se añade al lado.",
372
+ "agent.calendarYourTime": "{when} en tu hora",
355
373
  "agent.calendarUnreadable": "Ninguno de los horarios de este agente se puede leer como expresión cron, así que no hay nada planificado.",
356
374
  "agent.log": "Ejecuciones",
357
375
  "agent.logEmpty": "Este agente todavía no se ha ejecutado.",
@@ -0,0 +1,40 @@
1
+ import { Info } from "lucide-react";
2
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
3
+
4
+ /**
5
+ * An explanation behind an ⓘ, beside the thing it explains (#204, shared since #249).
6
+ *
7
+ * It exists for the sentence that answers "what IS this" — read once, then furniture. Not for the
8
+ * sentence you need WHILE you type: `agent.cronHint` ("five fields: minute, hour, …") deliberately
9
+ * stays a visible paragraph, because a reference you read while writing must not be behind a hover.
10
+ * That is the dividing line: *what is this* may hide, *how do I write this* may not.
11
+ *
12
+ * ⚠️ The hint is the trigger's accessible NAME, not only the tooltip's content: Radix describes the
13
+ * trigger by the content only while it is open, and a listener who tabs past a nameless icon button
14
+ * would never learn there is anything behind it.
15
+ *
16
+ * ⚠️ A real `<button>`, not a bare icon or a `<span>` with a hover style. A tooltip that only opens
17
+ * on hover cannot be opened at all on a touch device, and an element that is not focusable is one
18
+ * the keyboard walks straight past.
19
+ *
20
+ * ⚠️ It does NOT replace a field's description. Where this sits beside a form control, the control
21
+ * keeps its own `aria-describedby` pointing at an `sr-only` copy of the same words — the tooltip's
22
+ * content is in the accessibility tree only while it is open, so a field that gave up its
23
+ * description for one would quietly have lost it (#249).
24
+ */
25
+ export function SectionHint({ hint }: { hint: string }) {
26
+ return (
27
+ <TooltipProvider delayDuration={300}>
28
+ <Tooltip>
29
+ <TooltipTrigger
30
+ type="button"
31
+ aria-label={hint}
32
+ className="rounded-full text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
33
+ >
34
+ <Info aria-hidden="true" className="size-3.5" />
35
+ </TooltipTrigger>
36
+ <TooltipContent className="max-w-xs">{hint}</TooltipContent>
37
+ </Tooltip>
38
+ </TooltipProvider>
39
+ );
40
+ }
@@ -1,10 +1,17 @@
1
1
  import { createContext, type ReactNode, useContext, useMemo, useState } from "react";
2
- import { browserTimezone, readTimezone, rememberTimezone } from "./timezone.ts";
2
+ import { browserTimezone, readChosenTimezone, rememberTimezone } from "./timezone.ts";
3
3
 
4
4
  // The React seam of the timezone preference, the same shape as i18n-context and theme-context.
5
5
 
6
6
  export interface TimezoneSelection {
7
+ /** The zone to read times in: the choice, or the browser's own where there is none. */
7
8
  current: string;
9
+ /**
10
+ * ⚠️ The zone somebody actually CHOSE — `null` while they never did (#256). `current` cannot
11
+ * answer this: it falls back to the browser, and a screen that treated that as a preference would
12
+ * annotate every schedule with a guess nobody made.
13
+ */
14
+ chosen: string | null;
8
15
  select(zone: string): void;
9
16
  }
10
17
 
@@ -16,17 +23,18 @@ interface TimezoneProviderProps {
16
23
  }
17
24
 
18
25
  export function TimezoneProvider({ store, children }: TimezoneProviderProps) {
19
- const [current, setCurrent] = useState(() => readTimezone(store));
26
+ const [chosen, setChosen] = useState(() => readChosenTimezone(store));
20
27
 
21
28
  const selection = useMemo<TimezoneSelection>(
22
29
  () => ({
23
- current,
30
+ current: chosen ?? browserTimezone(),
31
+ chosen,
24
32
  select: (zone) => {
25
33
  rememberTimezone(zone, store);
26
- setCurrent(zone);
34
+ setChosen(zone);
27
35
  },
28
36
  }),
29
- [current, store],
37
+ [chosen, store],
30
38
  );
31
39
 
32
40
  return <TimezoneContext value={selection}>{children}</TimezoneContext>;
@@ -43,3 +51,15 @@ export function useTimezone(): string {
43
51
  const selection = useContext(TimezoneContext);
44
52
  return selection?.current ?? browserTimezone();
45
53
  }
54
+
55
+ /**
56
+ * The zone the reader CHOSE, or `null` (#256).
57
+ *
58
+ * ⚠️ Not `useTimezone`. This is for screens that add the reader's own reading BESIDE a time that is
59
+ * already correct — the agent's calendar. Nothing may be added for somebody who never set a zone:
60
+ * the browser's guess presented as "your time" is a setting the reader did not make, and it would
61
+ * put a second time on every row of a screen that was fine as it was.
62
+ */
63
+ export function useChosenTimezone(): string | null {
64
+ return useContext(TimezoneContext)?.chosen ?? null;
65
+ }
@@ -39,16 +39,31 @@ export function isKnownTimezone(zone: string): boolean {
39
39
  }
40
40
  }
41
41
 
42
- // A stored zone the engine no longer knows is discarded rather than kept: IANA renames zones, and a
43
- // name that stopped resolving would otherwise throw on every date the calendar draws.
44
- export function readTimezone(store?: Pick<Storage, "getItem">): string {
42
+ /**
43
+ * The zone the reader actually CHOSE, or `null` when they never did.
44
+ *
45
+ * ⚠️ The difference from `readTimezone` is the whole reason this exists (#256). A screen that wants
46
+ * to add "…and this is what it says on your clock" may only do so for a zone somebody set: falling
47
+ * back to the browser would put a guess on screen dressed as a preference, and the agent's calendar
48
+ * would start annotating every schedule for a reader who never asked for it.
49
+ *
50
+ * A stored zone the engine no longer knows counts as "never chose": IANA renames zones, and a name
51
+ * that stopped resolving would otherwise throw on every date the calendar draws.
52
+ */
53
+ export function readChosenTimezone(store?: Pick<Storage, "getItem">): string | null {
45
54
  let stored: string | null = null;
46
55
  try {
47
56
  stored = store?.getItem(STORE_KEY) ?? null;
48
57
  } catch {
49
- return browserTimezone();
58
+ return null;
50
59
  }
51
- return stored !== null && isKnownTimezone(stored) ? stored : browserTimezone();
60
+ return stored !== null && isKnownTimezone(stored) ? stored : null;
61
+ }
62
+
63
+ // The zone to read times in — the choice, and the browser's own where there is none. Never null: a
64
+ // component that has to format a date needs an answer, and the browser's is the honest one.
65
+ export function readTimezone(store?: Pick<Storage, "getItem">): string {
66
+ return readChosenTimezone(store) ?? browserTimezone();
52
67
  }
53
68
 
54
69
  export function rememberTimezone(zone: string, store?: Pick<Storage, "setItem">): void {
@@ -11,10 +11,11 @@ import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu
11
11
  * the rule could be forgotten. That is the whole point: the menu holds rename, move, share and
12
12
  * archive, and a position one has to look for is a position one stops using.
13
13
  *
14
- * The two slots are for what belongs to this thing but lives further down the tree, where its state
15
- * is. `title-meta` is a quiet word beside the name (a table's row count); `title-actions` is a
16
- * button in the group (saving a document, exporting a table). Both are filled through `ActionSlot`
17
- * and both sit before the menu for the same reason `children` do.
14
+ * The three slots are for what belongs to this thing but lives further down the tree, where its
15
+ * state is. `title-meta` is a quiet word beside the name (a table's row count); `title-byline` is a
16
+ * quiet line under it (who made an agent, #258); `title-actions` is a button in the group (saving a
17
+ * document, exporting a table). All are filled through `ActionSlot`, and the two in the group sit
18
+ * before the menu for the same reason `children` do.
18
19
  */
19
20
  export function TitleRow({
20
21
  title,
@@ -47,6 +48,10 @@ export function TitleRow({
47
48
  <h2 className="truncate text-lg font-semibold">{title}</h2>
48
49
  <span data-slot="title-meta" className="text-sm text-muted-foreground" />
49
50
  </div>
51
+ {/* ⚠️ `empty:hidden` is the whole rule for "no line rather than an empty one": nothing is
52
+ portalled in here for most kinds, and a slot that kept its margin would make every
53
+ head a few pixels taller for a line nobody wrote (#258). */}
54
+ <span data-slot="title-byline" className="mt-0.5 block min-w-0 empty:hidden" />
50
55
  {description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
51
56
  </div>
52
57
  </div>
@@ -0,0 +1,41 @@
1
+ import type { SessionUser } from "@anchrd/intel-contract";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { useIntelRouterContext } from "@/router/router-context.ts";
4
+
5
+ /**
6
+ * The signed-in person, from the one query the shell already makes.
7
+ *
8
+ * ⚠️ The same query key the user footer uses, so asking here costs no second request — and the two
9
+ * can never disagree about who is signed in.
10
+ */
11
+ export function useSessionUser(): SessionUser | null {
12
+ const { data } = useIntelRouterContext();
13
+ const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
14
+ return session.data ?? null;
15
+ }
16
+
17
+ /** What a person is CALLED, by the same rule the user footer follows: the name, or the address. */
18
+ export function displayNameOf(user: SessionUser): string {
19
+ return user.name ?? user.email;
20
+ }
21
+
22
+ /**
23
+ * The display name behind a user id — or `null` where this browser cannot say.
24
+ *
25
+ * ⚠️ Today it can say for exactly one id: the signed-in person's. Intel has no user directory —
26
+ * `/session` answers from the token's own subject and deliberately carries nothing else, and the
27
+ * Gate SDK Intel holds (`authorize`, `interfaces`) has no lookup either. So a foreign id is not
28
+ * "probably a person whose name we could guess": it is unresolvable, and every caller here has to
29
+ * treat it as such.
30
+ *
31
+ * ⚠️ Never fall back to the id. An id under a name is not an answer to "who made this", it is the
32
+ * question again in smaller type — and it would leak an identifier onto a screen that has no reason
33
+ * to carry one. Callers show nothing instead (#258).
34
+ *
35
+ * This is the ONE place that changes when Intel gains a way to resolve a name (#261).
36
+ */
37
+ export function useUserName(userId: string | null): string | null {
38
+ const me = useSessionUser();
39
+ if (userId === null || me === null || me.id !== userId) return null;
40
+ return displayNameOf(me);
41
+ }
package/tsconfig.json CHANGED
@@ -7,5 +7,11 @@
7
7
  "paths": { "@/*": ["src/*"] },
8
8
  "types": ["vite/client"]
9
9
  },
10
- "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
10
+ "include": [
11
+ "src/**/*.ts",
12
+ "src/**/*.tsx",
13
+ "vite.config.ts",
14
+ "vitest.config.ts",
15
+ "vitest.setup.ts"
16
+ ]
11
17
  }