@anchrd/intel-ui 0.15.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-ui",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -60,6 +60,7 @@
60
60
  "zod": "^4.4.3"
61
61
  },
62
62
  "devDependencies": {
63
+ "@testing-library/dom": "^10.4.1",
63
64
  "@testing-library/react": "^16.3.2",
64
65
  "@types/node": "^24.13.3",
65
66
  "@types/react": "^19.2.17",
@@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query";
3
3
  import { ChevronDown, ChevronRight } from "lucide-react";
4
4
  import { useState } from "react";
5
5
  import {
6
- costWindow,
7
6
  formatCost,
8
7
  formatTokens,
9
8
  type RunCost,
@@ -23,9 +22,10 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
23
22
  * trimmed there. So this list is short by design and has no paging — the runtime answers at most a
24
23
  * hundred runs and keeps no more.
25
24
  *
26
- * ⚠️ A run says what it did and, when it failed, what stopped it. It does not carry the answer it
27
- * produced: a chat answer belongs to the person it was streamed to and a mail answer to the
28
- * correspondent, and the runtime deliberately stores neither where this list could reach it.
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
29
  *
30
30
  * ⚠️ Since #251 it also says what each run consumed and what it cost — and the two come from
31
31
  * DIFFERENT places on purpose. The tokens are the runtime's own record; the money is Cloudflare's
@@ -223,22 +223,52 @@ function RunRow({
223
223
  ) : null}
224
224
  </span>
225
225
  </button>
226
- {open ? <RunDetail agentId={agentId} runId={run.id} /> : null}
226
+ {open ? <RunDetail agentId={agentId} run={run} /> : null}
227
227
  </li>
228
228
  );
229
229
  }
230
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
+
231
259
  // The operational log of one run: what it tried, in order. Fetched only when a row is opened —
232
260
  // every run carries its own entries and loading all of them would be the whole log for one screen.
233
- function RunDetail({ agentId, runId }: { agentId: string; runId: string }) {
261
+ function RunDetail({ agentId, run }: { agentId: string; run: AgentRun }) {
234
262
  const { data } = useIntelRouterContext();
235
263
  const i18n = useI18n();
264
+ const runId = run.id;
236
265
  const detail = useQuery({
237
266
  queryKey: ["agent-run", agentId, runId],
238
267
  queryFn: () => data.getAgentRun(agentId, runId),
239
268
  });
240
269
  return (
241
270
  <div className="border-t px-4 py-3">
271
+ <RunAnswer run={run} i18n={i18n} />
242
272
  {detail.isPending ? (
243
273
  <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
244
274
  ) : null}
@@ -340,8 +340,12 @@ function defaultRoleFor(kind: PickerKind): AgentReferenceRole | null {
340
340
  * changes what a document MEANS to an agent must look changeable.
341
341
  *
342
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 WRITES into
344
- * one and SEARCHES the other — so a document simply never offers them.
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.
345
349
  */
346
350
  function KnowledgeSection({
347
351
  definition,
@@ -43,6 +43,18 @@ export const AgentRun = z.object({
43
43
  finishedAt: z.string().nullable(),
44
44
  steps: z.number(),
45
45
  error: z.string().nullable(),
46
+ /**
47
+ * What the run answered, for the runs nobody was watching: `schedule`, `manual` and `task`.
48
+ *
49
+ * ⚠️ A `chat` or `mail` run carries none, and that is a runtime invariant rather than an accident
50
+ * (#268): that answer belongs to the person whose window it was streamed into or to the
51
+ * correspondent it was posted to. The log screen therefore claims nothing at all on those two
52
+ * triggers — an empty state there would read as "the answer went missing".
53
+ *
54
+ * ⚠️ `.nullish()` for the same reason as `usage` below: a runtime from before #268 sends no such
55
+ * field, and a required one would blank the whole Log tab over its absence.
56
+ */
57
+ answer: z.string().nullish(),
46
58
  // ⚠️ `.nullish()` on both, not `.nullable()`. The runtime is a separate deployment and may be a
47
59
  // version BEHIND this page: a run list from a runtime without #250/#251 has no such field at all,
48
60
  // and a required one would blank the whole Log tab over a missing number.
package/src/i18n/de.json CHANGED
@@ -322,7 +322,7 @@
322
322
  "agent.contactAdd": "Erreichbarkeit hinzufügen",
323
323
  "agent.contactAddReason": "Eine Mail-Adresse gehört der Installation, nicht der Definition des Agenten — sie wird dort konfiguriert, wo die Laufzeit betrieben wird.",
324
324
  "agent.knows": "Was er weiß",
325
- "agent.knowsHint": "Was dieser Agent liest, und wofür jeder Eintrag zählt. Eine Systemnachricht weist ihn an — ein Ordner, ein Dokument oder eine Tabelle. Semantischer Kontext wird durchsucht und in das Gedächtnis wird zurückgeschrieben, deshalb sind beide Ordner.",
325
+ "agent.knowsHint": "Was dieser Agent liest, und wofür jeder Eintrag zählt. Eine Systemnachricht weist ihn an — ein Ordner, ein Dokument oder eine Tabelle. Semantischer Kontext wird durchsucht, und in das Gedächtnis wird zurückgeschrieben und es wird ebenso durchsucht, deshalb sind beide Ordner. Der Name verspricht weniger als die Rolle: Was der Agent sich im Gedächtnis notiert, kommt ihm bei jeder späteren Suche wieder entgegen.",
326
326
  "agent.addReference": "Etwas zum Lesen hinzufügen",
327
327
  "agent.removeReferenceOf": "{title} nicht mehr lesen",
328
328
  "agent.pickEntry": "Was gelesen wird",
@@ -377,6 +377,8 @@
377
377
  "agent.runFailed": "Dieser Lauf konnte nicht gelesen werden.",
378
378
  "agent.runLog": "Was dieser Lauf getan hat",
379
379
  "agent.runLogEmpty": "Dieser Lauf hat nichts aufgezeichnet.",
380
+ "agent.runAnswer": "Was dieser Lauf geantwortet hat",
381
+ "agent.runAnswerEmpty": "Dieser Lauf hat keine Antwort aufgezeichnet.",
380
382
  "agent.steps": "{count} Schritte",
381
383
  "agent.trigger.chat": "aus dem Chat",
382
384
  "agent.trigger.schedule": "nach Zeitplan",
package/src/i18n/en.json CHANGED
@@ -322,7 +322,7 @@
322
322
  "agent.contactAdd": "Add contact",
323
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.",
324
324
  "agent.knows": "What it knows",
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.",
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 and searched as well, so both of those are folders. The name promises less than the role does: what the agent notes down in memory comes back to it in every later search.",
326
326
  "agent.addReference": "Add something to read",
327
327
  "agent.removeReferenceOf": "Stop reading {title}",
328
328
  "agent.pickEntry": "What to read",
@@ -377,6 +377,8 @@
377
377
  "agent.runFailed": "This run could not be read.",
378
378
  "agent.runLog": "What this run did",
379
379
  "agent.runLogEmpty": "This run recorded nothing.",
380
+ "agent.runAnswer": "What this run answered",
381
+ "agent.runAnswerEmpty": "This run recorded no answer.",
380
382
  "agent.steps": "{count} steps",
381
383
  "agent.trigger.chat": "from the chat",
382
384
  "agent.trigger.schedule": "on schedule",
package/src/i18n/es.json CHANGED
@@ -322,7 +322,7 @@
322
322
  "agent.contactAdd": "Añadir un contacto",
323
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.",
324
324
  "agent.knows": "Lo que sabe",
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.",
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 y también se busca, por eso ambas son carpetas. El nombre promete menos que el papel: lo que el agente anota en la memoria le vuelve en cada búsqueda posterior.",
326
326
  "agent.addReference": "Añadir algo para leer",
327
327
  "agent.removeReferenceOf": "Dejar de leer {title}",
328
328
  "agent.pickEntry": "Qué se lee",
@@ -377,6 +377,8 @@
377
377
  "agent.runFailed": "Esta ejecución no se ha podido leer.",
378
378
  "agent.runLog": "Lo que ha hecho esta ejecución",
379
379
  "agent.runLogEmpty": "Esta ejecución no ha registrado nada.",
380
+ "agent.runAnswer": "Lo que ha respondido esta ejecución",
381
+ "agent.runAnswerEmpty": "Esta ejecución no ha registrado ninguna respuesta.",
380
382
  "agent.steps": "{count} pasos",
381
383
  "agent.trigger.chat": "desde el chat",
382
384
  "agent.trigger.schedule": "por horario",