@zq-silk/yui 2.5.0 → 2.6.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 (38) hide show
  1. package/dist/cli/commandCatalog.js +3 -2
  2. package/dist/commands/taskCommands.js +8 -3
  3. package/dist/commands/taskContextCommand.js +3 -2
  4. package/dist/context/taskContext.js +7 -4
  5. package/dist/kernel/builtinCapabilities.js +2 -1
  6. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  7. package/dist/runtime/providerRetry.js +38 -0
  8. package/dist/runtime/taskSessions.js +240 -0
  9. package/dist/scheduler/taskExecutionProjection.js +78 -6
  10. package/dist/web/assets/assetManifest.js +2 -0
  11. package/dist/web/assets/client/app/taskView.js +1 -0
  12. package/dist/web/assets/client/domain/records.js +70 -2
  13. package/dist/web/assets/client/domain/runs.js +3 -2
  14. package/dist/web/assets/client/domain/vocab.js +1 -1
  15. package/dist/web/assets/client/i18n/en.js +17 -1
  16. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  17. package/dist/web/assets/client/lib/api.js +4 -1
  18. package/dist/web/assets/client/lib/theme.js +31 -10
  19. package/dist/web/assets/client/views/task/actions.js +38 -21
  20. package/dist/web/assets/client/views/task/observation.js +12 -3
  21. package/dist/web/assets/client/views/task/overview.js +7 -40
  22. package/dist/web/assets/client/views/task/runtime.js +71 -34
  23. package/dist/web/assets/shared/icons.js +2 -0
  24. package/dist/web/assets/shell/index.js +1 -0
  25. package/dist/web/assets/shell/settings.js +1 -0
  26. package/dist/web/assets/styles/views/task.js +14 -0
  27. package/dist/web/webServer.js +12 -0
  28. package/dist/web/webSessions.js +2 -165
  29. package/dist/web/webSnapshot.js +9 -5
  30. package/dist/web/webTaskActivity.js +54 -0
  31. package/dist/web/webTaskSurface.js +1 -0
  32. package/docs/observability/README.md +25 -1
  33. package/docs/observability/README.zh-CN.md +18 -1
  34. package/docs/provider-retry.md +11 -0
  35. package/docs/release-workflow.md +27 -0
  36. package/docs/release-workflow.zh-CN.md +22 -0
  37. package/package.json +1 -1
  38. package/skills/yui-runtime/references/recovery.md +9 -0
@@ -4,6 +4,7 @@
4
4
  import { readFileSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { APP_SCRIPT, CLIENT_MODULES } from "./client/index.js";
7
+ import { THEME_BOOTSTRAP_SCRIPT } from "./client/lib/theme.js";
7
8
  import { FONT_WOFF2_BASE64 } from "./fontData.js";
8
9
  import { DASHBOARD_HTML } from "./shell/index.js";
9
10
  import { STYLESHEETS } from "./styles/index.js";
@@ -13,6 +14,7 @@ const JS = "text/javascript; charset=utf-8";
13
14
  export const WEB_ASSETS = Object.freeze({
14
15
  ...Object.fromEntries(STYLESHEETS.map(({ name, body }) => [`/assets/css/${name}.css`, { contentType: CSS, body }])),
15
16
  ...Object.fromEntries(Object.entries(CLIENT_MODULES).map(([path, body]) => [`/assets/js/${path}.js`, { contentType: JS, body }])),
17
+ "/assets/theme.js": { contentType: JS, body: THEME_BOOTSTRAP_SCRIPT },
16
18
  "/assets/app.js": { contentType: JS, body: APP_SCRIPT },
17
19
  ...fontAssets(),
18
20
  "/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", JS),
@@ -154,6 +154,7 @@ function detailContext(view) {
154
154
  taskAction: deps.api.taskAction,
155
155
  inspect: api.inspect,
156
156
  list: api.list,
157
+ activity: api.activity,
157
158
  artifacts: api.artifacts,
158
159
  readArtifact: api.artifact,
159
160
  evidence: api.evidence,
@@ -4,9 +4,77 @@ export const RECORDS_SCRIPT = String.raw `
4
4
  // never acknowledges or accepts anything.
5
5
  import { h, icon } from "/assets/js/lib/dom.js";
6
6
  import { button, jsonBlock, mono, note } from "/assets/js/ui/primitives.js";
7
- import { rawDisclosure } from "/assets/js/ui/containers.js";
7
+ import { rawDisclosure, listCard, setCardCount } from "/assets/js/ui/containers.js";
8
8
  import { richText } from "/assets/js/ui/text.js";
9
- import { cachedExact, readExact, summaryFields } from "/assets/js/domain/context.js";
9
+ import { cachedExact, readExact, summaryFields, entriesOf, totalOf, exactCache } from "/assets/js/domain/context.js";
10
+
11
+ // Current-family counts and pages use exactly the Context current filter.
12
+ // A changed Context revalidates already-loaded membership with one bounded
13
+ // page. Only the family's own digest invalidates its pages, not unrelated
14
+ // runtime events. Loading a list does not eagerly inspect its bodies.
15
+ export function currentRecordCard(data, store, t, actions, options) {
16
+ const view = data.viewState;
17
+ exactCache(view);
18
+ view.openRows = view.openRows || {};
19
+ view.currentLists = view.currentLists || {};
20
+ let state = view.currentLists[store];
21
+ if (!state || (!state.started && state.key !== data.core.coreCursor)) {
22
+ state = view.currentLists[store] = {
23
+ key: data.core.coreCursor, items: entriesOf(data.core, store),
24
+ total: totalOf(data.core, store), started: false, cursor: null, error: null, pending: false
25
+ };
26
+ }
27
+ const revalidate = state.key !== data.core.coreCursor;
28
+ state.key = data.core.coreCursor;
29
+ const element = listCard({ id: options.id, title: options.title, icon: options.icon || "pulse", count: state.total });
30
+ const rows = h("div.stack"), footer = h("div.stack", { "aria-live": "polite" });
31
+ element.list.append(rows, footer);
32
+ function draw(updateRows) {
33
+ setCardCount(element, state.total);
34
+ if (updateRows !== false) {
35
+ rows.replaceChildren();
36
+ state.items.forEach(function (item) {
37
+ rows.append(!item.omitted && item.value && options.render ? options.render(item.value, null, item)
38
+ : lazyRow(item, data.task.id, t, actions, {
39
+ cache: view.exact,
40
+ openRows: view.openRows,
41
+ head: h("span", null, item.ref.refId + (item.summary ? " · " + item.summary : "")),
42
+ render: options.render
43
+ }));
44
+ });
45
+ }
46
+ footer.replaceChildren(note(t("activity.loaded") + " " + state.items.length + " / " + state.total));
47
+ if (!state.total) footer.append(note(t("activity.none")));
48
+ if (state.error) footer.append(note(state.error + " · " + t("activity.refreshHint"), "bad"));
49
+ if ((!state.started && state.items.length < state.total) || state.cursor) {
50
+ footer.append(button(t("activity.more") + " (" + Math.max(0, state.total - state.items.length) + ")", {
51
+ disabled: state.pending, onClick: function () { load(state.started); }
52
+ }));
53
+ }
54
+ footer.append(button(t("activity.refresh"), { disabled: state.pending, onClick: function () { load(false); } }));
55
+ element.dataset.reading = String(state.pending);
56
+ }
57
+ async function load(more) {
58
+ if (state.pending) return;
59
+ const busy = function () { return !!rows.querySelector('[data-unsent="true"], [data-reading="true"]'); };
60
+ if (busy()) { state.error = t("activity.busy"); draw(false); return; }
61
+ state.pending = true;
62
+ draw(false);
63
+ try {
64
+ const page = await actions.list(data.task.id, store, { view: "current", limit: 20, cursor: more ? state.cursor : null });
65
+ if (busy()) throw new Error(t("activity.busy"));
66
+ if (more || !state.started || state.digest !== page.digest) {
67
+ state.items = more ? state.items.concat(page.items) : page.items;
68
+ state.cursor = page.nextCursor;
69
+ }
70
+ state.digest = page.digest; state.total = page.total; state.started = true; state.error = null;
71
+ } catch (error) { state.error = error.message; }
72
+ finally { state.pending = false; draw(!state.error); }
73
+ }
74
+ draw();
75
+ if (revalidate) load(false);
76
+ return element;
77
+ }
10
78
 
11
79
  // A row drawn from its exact value: synchronously when cached, otherwise the
12
80
  // summary card until the read returns and replaces it in place.
@@ -16,7 +16,7 @@ export function runCard(run, t, locale) {
16
16
  run.purpose ? chip(label(t, "run.purpose", run.purpose)) : null,
17
17
  run.workItemId ? chip(run.workItemId) : null,
18
18
  h("span.spacer"),
19
- statusBadge(t, "run", "run", run.status)), runBody(run, t, locale));
19
+ statusBadge(t, "run", "run", run.status)), ...runBody(run, t, locale));
20
20
  return element;
21
21
  }
22
22
 
@@ -107,7 +107,8 @@ export function roleCard(role, t, locale, options) {
107
107
  function roleFacts(role, t, locale, opts) {
108
108
  const session = opts.runtimeRole && opts.runtimeRole.runtimeSession;
109
109
  const rows = [];
110
- rows.push([t("role.session"), session && session.nativeSessionId ? mono(session.nativeSessionId) : t("role.noSession")]);
110
+ rows.push([t("role.session"), session && session.nativeSessionId ? mono(session.nativeSessionId)
111
+ : !opts.runtimeRole ? t("activity.notLoaded") : t("role.noSession")]);
111
112
  if (role.launchRevision !== undefined) rows.push([t("role.desired"), "r" + role.launchRevision + (opts.launchDrift ? " · " + t("role.drift") : "")]);
112
113
  if (role.defaultAccess !== undefined) rows.push([t("role.access"), role.defaultAccess]);
113
114
  if (opts.retry) rows.push([t("role.providerRetry"), label(t, "retry", opts.retry.status) + " · "
@@ -22,7 +22,7 @@ export function label(t, prefix, value) {
22
22
  const TONES = {
23
23
  task: { draft: "warn", active: "info", completed: "ok", cancelled: "idle", archived: "idle" },
24
24
  exec: {
25
- "needs-leader-action": "accent", "waiting-on-agents": "info", "waiting-user": "warn",
25
+ "needs-leader-action": "accent", "waiting-on-agents": "info", "waiting-on-jobs": "info", "waiting-user": "warn",
26
26
  recovering: "warn", attention: "bad", "progressing-with-attention": "warn", blocked: "bad",
27
27
  conflicted: "warn", working: "info", completed: "ok", retired: "idle", cancelled: "idle",
28
28
  accepted: "ok", open: "info", archived: "idle"
@@ -58,7 +58,10 @@ export const EN = {
58
58
  "create.hint": "Planning saves a Draft and queues the Leader. It does not start development. Uncheck to save only.",
59
59
  "create.open": "Open saved task",
60
60
  "workbench.next": "Next step",
61
- "workbench.continue": "Continue with the current Leader",
61
+ "workbench.continue": "Talk to Leader",
62
+ "workbench.prompt.running": "Talk to the current Leader to adjust direction or add information.",
63
+ "workbench.prompt.activate": "Start the Task once the draft and plan are confirmed, or keep discussing with the Leader.",
64
+ "workbench.prompt.archive": "This Task has ended. Archive it, or follow up with the Leader.",
62
65
  "workbench.activate": "Start task",
63
66
  "workbench.activateConfirm": "Have you confirmed the Draft and plan? Request activation now?",
64
67
  "workbench.activateHint": "Uses the existing activation process. A saved request is not an active Task.",
@@ -700,6 +703,19 @@ export const EN = {
700
703
  "priority.low": "Low",
701
704
  "exec.status.needs-leader-action": "Needs Leader action",
702
705
  "exec.status.waiting-on-agents": "Waiting on agents",
706
+ "exec.status.waiting-on-jobs": "Waiting on background jobs",
707
+ "exec.action.wait-for-jobs": "Wait for background jobs",
708
+ "activity.loaded": "Loaded",
709
+ "activity.none": "No current items in this source.",
710
+ "activity.notLoaded": "Not loaded. Use the current activity list to read more.",
711
+ "activity.more": "Load more",
712
+ "activity.refresh": "Refresh list",
713
+ "activity.refreshHint": "If the source changed, refresh before continuing.",
714
+ "activity.busy": "Finish or clear the unsent answer, or wait for the current read before replacing this list.",
715
+ "activity.reviews": "Unfinished reviews",
716
+ "activity.native": "Native activity",
717
+ "activity.openSession": "Open current Role Session",
718
+ "activity.scope": "Partial coverage: selected native Sessions and retained, exactly identified child observations. Not a live process or complete historical Session inventory. Native facts may describe an AgentRun above; counts must not be added together.",
703
719
  "exec.status.waiting-user": "Waiting on you",
704
720
  "exec.status.recovering": "Recovering",
705
721
  "exec.status.attention": "Needs attention",
@@ -56,7 +56,10 @@ export const ZH_CN = {
56
56
  "create.hint": "保存 Draft 并通知 Leader 规划,不启动开发。取消勾选则仅保存任务。",
57
57
  "create.open": "打开已保存任务",
58
58
  "workbench.next": "下一步",
59
- "workbench.continue": "与当前 Leader 继续推进",
59
+ "workbench.continue": "与 Leader 对话",
60
+ "workbench.prompt.running": "需要调整方向或补充信息时,直接与当前 Leader 对话。",
61
+ "workbench.prompt.activate": "确认草稿与方案后启动任务,或继续与 Leader 讨论。",
62
+ "workbench.prompt.archive": "任务已结束。可以归档,或继续与 Leader 跟进。",
60
63
  "workbench.activate": "启动任务",
61
64
  "workbench.activateConfirm": "已确认草稿和方案,申请激活此任务?",
62
65
  "workbench.activateHint": "复用现有激活流程。请求已保存不代表任务已激活。",
@@ -698,6 +701,19 @@ export const ZH_CN = {
698
701
  "priority.low": "低",
699
702
  "exec.status.needs-leader-action": "待 Leader 处理",
700
703
  "exec.status.waiting-on-agents": "等待智能体",
704
+ "exec.status.waiting-on-jobs": "等待后台任务",
705
+ "exec.action.wait-for-jobs": "等待后台任务结果",
706
+ "activity.loaded": "已加载",
707
+ "activity.none": "此来源没有当前事项。",
708
+ "activity.notLoaded": "尚未加载;可通过当前活动列表继续读取。",
709
+ "activity.more": "继续加载",
710
+ "activity.refresh": "刷新列表",
711
+ "activity.refreshHint": "若来源已变化,请刷新后继续。",
712
+ "activity.busy": "请先完成或清空未发送的回答,或等待当前读取结束,再替换列表。",
713
+ "activity.reviews": "未结束的审查",
714
+ "activity.native": "原生活动",
715
+ "activity.openSession": "打开该角色的当前会话",
716
+ "activity.scope": "部分覆盖:选中的原生会话,以及身份明确、尚未结束的保留子活动观测;不是实时进程或完整历史会话清单。原生事实可能与上方 AgentRun 描述同一执行,计数不可直接相加。",
701
717
  "exec.status.waiting-user": "等待你回答",
702
718
  "exec.status.recovering": "恢复中",
703
719
  "exec.status.attention": "需要关注",
@@ -70,6 +70,9 @@ export const api = {
70
70
  context: function (taskId) { return requestJson(task(taskId) + "/context"); },
71
71
  workbench: function (taskId) { return requestJson(task(taskId) + "/workbench"); },
72
72
  observation: function (taskId) { return requestJson(task(taskId) + "?compact=true", { signal: AbortSignal.timeout(1500) }); },
73
+ activity: function (taskId, options) {
74
+ return requestJson(task(taskId) + "/activity?" + new URLSearchParams(options || {}));
75
+ },
73
76
  projects: function (after) { return requestJson("/api/projects" + (after ? "?after=" + encodeURIComponent(after) : "")); },
74
77
  createTask: function (input) { return submitMutation("task/create", "/api/tasks", input); },
75
78
  taskAction: function (taskId, action, requestId) {
@@ -99,7 +102,7 @@ export const api = {
99
102
  // One page of a Context family: summaries and exact references, no bodies.
100
103
  list: function (taskId, store, options) {
101
104
  const query = new URLSearchParams({ store: store });
102
- ["status", "cursor", "limit"].forEach(function (key) {
105
+ ["view", "status", "cursor", "limit"].forEach(function (key) {
103
106
  if (options && options[key] !== undefined && options[key] !== null) query.set(key, String(options[key]));
104
107
  });
105
108
  return requestJson(task(taskId) + "/list?" + query);
@@ -1,20 +1,41 @@
1
1
  import { DEFAULT_DARK_THEME, DEFAULT_LIGHT_THEME, THEME_OPTIONS } from "../../shared/themes.js";
2
+ // Shared by the parser-blocking head script and the later settings controller.
3
+ // Both resolve the same browser preference without publishing global state.
4
+ const THEME_PREFERENCE_SCRIPT = String.raw `
5
+ const IDS = ${JSON.stringify(THEME_OPTIONS.map(theme => theme.id))};
6
+ const DEFAULT_DARK = ${JSON.stringify(DEFAULT_DARK_THEME.id)};
7
+ const DEFAULT_LIGHT = ${JSON.stringify(DEFAULT_LIGHT_THEME.id)};
8
+
9
+ function storedPreference() {
10
+ try {
11
+ const saved = localStorage.getItem("yui.theme");
12
+ return IDS.includes(saved) ? saved : "system";
13
+ } catch { return "system"; }
14
+ }
15
+
16
+ function resolveTheme(preference, media) {
17
+ return preference === "system" ? (media.matches ? DEFAULT_LIGHT : DEFAULT_DARK) : preference;
18
+ }
19
+ `;
20
+ // A same-origin classic script runs before styles/body, unlike the deferred
21
+ // application module graph. No inline script or weaker CSP is needed.
22
+ export const THEME_BOOTSTRAP_SCRIPT = String.raw `
23
+ (function () {
24
+ ${THEME_PREFERENCE_SCRIPT}
25
+ document.documentElement.dataset.theme = resolveTheme(
26
+ storedPreference(), window.matchMedia("(prefers-color-scheme: light)")
27
+ );
28
+ })();
29
+ `;
2
30
  export const THEME_SCRIPT = String.raw `
3
31
  // Theme preference. "system" follows prefers-color-scheme and resolves to the
4
32
  // default dark or light theme; the resolved id is stamped on <html> so its
5
33
  // token block applies. Options and swatches come from shared/themes.ts.
6
34
  import { h, markSelected } from "/assets/js/lib/dom.js";
7
- import { readPreference, writePreference } from "/assets/js/lib/prefs.js";
35
+ import { writePreference } from "/assets/js/lib/prefs.js";
8
36
 
9
37
  const THEMES = Object.freeze(${JSON.stringify(THEME_OPTIONS)});
10
- const IDS = THEMES.map(function (theme) { return theme.id; });
11
- const DEFAULT_DARK = ${JSON.stringify(DEFAULT_DARK_THEME.id)};
12
- const DEFAULT_LIGHT = ${JSON.stringify(DEFAULT_LIGHT_THEME.id)};
13
-
14
- function storedPreference() {
15
- const saved = readPreference("yui.theme", "system");
16
- return IDS.includes(saved) ? saved : "system";
17
- }
38
+ ${THEME_PREFERENCE_SCRIPT}
18
39
 
19
40
  function themeOption(theme, t, onPick) {
20
41
  const swatch = h("span.theme-swatch", { "aria-hidden": "true" }, theme.swatch.map(function (color, index) {
@@ -36,7 +57,7 @@ export function createThemeController(container, t) {
36
57
  const listeners = new Set();
37
58
 
38
59
  function resolved() {
39
- return preference === "system" ? (media.matches ? DEFAULT_LIGHT : DEFAULT_DARK) : preference;
60
+ return resolveTheme(preference, media);
40
61
  }
41
62
  function apply() {
42
63
  document.documentElement.dataset.theme = resolved();
@@ -1,36 +1,53 @@
1
1
  export const TASK_ACTIONS_SCRIPT = String.raw `
2
+ // Next step: one compact action bar. A short prompt on the left, the actions
3
+ // on the right (secondary first, primary last), and a single status line
4
+ // below that appears only when it carries a condition, a block or a receipt.
2
5
  import { h } from "/assets/js/lib/dom.js";
3
- import { button, note } from "/assets/js/ui/primitives.js";
6
+ import { button } from "/assets/js/ui/primitives.js";
4
7
  import { card } from "/assets/js/ui/containers.js";
5
8
  import { receiptLine, setReceipt, submitWrite } from "/assets/js/ui/forms.js";
6
9
 
7
10
  export function taskActions(task, t, ctx) {
8
- const element = card({ title: t("workbench.next"), icon: "chat" });
9
- element.body.append(button(t("workbench.continue"), { variant: "primary", onClick: function () { ctx.openConversation(); } }));
10
11
  const action = task.status === "draft" ? "activate"
11
12
  : ["completed", "cancelled"].includes(task.status) ? "archive" : null;
13
+ const element = card({ className: "next-card" });
14
+ const continueButton = button(t("workbench.continue"), {
15
+ icon: "chat", variant: action ? "" : "primary", onClick: function () { ctx.openConversation(); }
16
+ });
17
+ const buttons = h("div.next-actions", null, continueButton);
18
+ const lead = h("div.next-lead", null,
19
+ h("p.next-kicker", null, t("workbench.next")),
20
+ h("p.next-prompt", null, t("workbench.prompt." + (action || "running"))));
21
+ element.body.append(h("div.next-row", null, lead, buttons));
12
22
  if (!action) return element;
23
+
13
24
  const activation = task.activationRequest;
14
25
  const blocked = action === "activate" && (task.executionGate.state !== "enabled"
15
26
  || activation && ["pending", "failed"].includes(activation.disposition));
16
- const receipt = receiptLine(blocked ? (activation && activation.outcome || t("workbench.activationBlocked")) : t("receipt.notSubmitted"));
17
- const control = button(t("workbench." + action), { disabled: !!blocked, onClick: function () {
18
- if (!window.confirm(t("workbench." + action + "Confirm"))) return;
19
- submitWrite({
20
- control: control, receipt: receipt, form: element.body, t: t,
21
- send: function (requestId) { return ctx.taskAction(task.id, action, requestId); },
22
- saved: function (value) {
23
- const result = value.result;
24
- const text = action === "activate" ? result.operationRef + " · " + result.request.disposition
25
- : result.task.id + " · " + result.task.status;
26
- setReceipt(receipt, text, "ok");
27
- return true;
28
- },
29
- rejectedKey: "receipt.notSubmittedBecause", unknownKey: "workbench.unknown",
30
- afterWrite: ctx.afterWrite
31
- });
32
- } });
33
- element.body.append(control, note(t("workbench." + action + "Hint")), receipt);
27
+ // Idle receipts stay empty (hidden); only real state is shown.
28
+ const receipt = receiptLine(blocked ? (activation && activation.outcome || t("workbench.activationBlocked")) : "", blocked ? "warn" : "");
29
+ const control = button(t("workbench." + action), {
30
+ icon: action === "activate" ? "play" : "archive", variant: "primary", disabled: !!blocked,
31
+ title: t("workbench." + action + "Hint"),
32
+ onClick: function () {
33
+ if (!window.confirm(t("workbench." + action + "Confirm"))) return;
34
+ submitWrite({
35
+ control: control, receipt: receipt, form: element.body, t: t,
36
+ send: function (requestId) { return ctx.taskAction(task.id, action, requestId); },
37
+ saved: function (value) {
38
+ const result = value.result;
39
+ const text = action === "activate" ? result.operationRef + " · " + result.request.disposition
40
+ : result.task.id + " · " + result.task.status;
41
+ setReceipt(receipt, text, "ok");
42
+ return true;
43
+ },
44
+ rejectedKey: "receipt.notSubmittedBecause", unknownKey: "workbench.unknown",
45
+ afterWrite: ctx.afterWrite
46
+ });
47
+ }
48
+ });
49
+ buttons.append(control);
50
+ element.body.append(h("div.next-foot", null, h("p.next-hint", null, t("workbench." + action + "Hint")), receipt));
34
51
  return element;
35
52
  }
36
53
  `;
@@ -73,6 +73,14 @@ function nowHead(data, execution, t, locale) {
73
73
  : h("span.now-owner", null, label(t, "exec.owner", execution.owner) + " · " + label(t, "exec.action", execution.action)));
74
74
  const activeCount = execution.activeRunCount === undefined ? (execution.activeRuns || []).length : execution.activeRunCount;
75
75
  if (activeCount) head.append(badge(activeCount + " " + t("now.activeRuns"), "info"));
76
+ if (execution.activity) {
77
+ const jobs = execution.activity.jobs, native = execution.activity.native;
78
+ head.append(badge(t("jobs.title") + " · " + t("job.running") + " " + jobs.running
79
+ + " / " + t("job.queued") + " " + jobs.queued + " / " + t("detail.unobserved") + " " + jobs.unknown, jobs.unknown ? "warn" : "info"));
80
+ head.append(badge(t("activity.native") + " · " + (native.active + native.waiting)
81
+ + " / " + t("sessions.background") + " " + native.background
82
+ + " / " + t("detail.unobserved") + " " + native.unknown, native.unknown ? "warn" : "info"));
83
+ }
76
84
  if (execution.monitoring === "stopped") head.append(badge(t("now.monitoringStopped"), "warn"));
77
85
  if (execution.failClosed) head.append(badge(t("now.failClosed"), "bad"));
78
86
  head.append(h("span.spacer"), h("span.faint.small", { title: formatDateTime(data.runtimeObservedAt, locale) },
@@ -123,7 +131,8 @@ function drawSessions(container, observation, t, locale) {
123
131
  function drawRoleSession(slot, session, observation, t, locale) {
124
132
  clear(slot);
125
133
  if (!session) {
126
- slot.append(h("span.faint.small", null, observation ? t("session.noneRecorded") : t("sessions.unavailable")));
134
+ slot.append(h("span.faint.small", null, !observation ? t("sessions.unavailable")
135
+ : observation.total > observation.sessions.length ? t("activity.notLoaded") : t("session.noneRecorded")));
127
136
  return;
128
137
  }
129
138
  slot.append(h("div.role-session-head", null, dot(tone("session", session.group)),
@@ -143,8 +152,8 @@ function drawSessionFacts(facts, observation, sessions, t, locale) {
143
152
  [t("sessions.role"), session.roleName],
144
153
  [t("sessions.lastActivity"), session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : t("sessions.unobserved")],
145
154
  [t("sessions.inputUpdated"), formatDateTime(session.sourceUpdatedAt, locale)],
146
- [t("sessions.operations"), session.operations.length ? session.operations.join(", ") : null],
147
- [t("sessions.background"), session.background.length ? session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ") : null],
155
+ [t("sessions.operations"), (session.operationCount ?? session.operations.length) + " · " + session.operations.join(", ")],
156
+ [t("sessions.background"), (session.backgroundCount ?? session.background.length) + " · " + session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ")],
148
157
  [t("sessions.identity"), mono([session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / "))]
149
158
  ]));
150
159
  });
@@ -7,8 +7,8 @@ import { formatDateTime } from "/assets/js/lib/format.js";
7
7
  import { bulletList, button, codeBlock, dot, emptyState, note, timeTag } from "/assets/js/ui/primitives.js";
8
8
  import { card, disclosure } from "/assets/js/ui/containers.js";
9
9
  import { richText } from "/assets/js/ui/text.js";
10
- import { entriesOf, exactCache, totalOf, valuesOf } from "/assets/js/domain/context.js";
11
- import { lazyRow, recordCard } from "/assets/js/domain/records.js";
10
+ import { entriesOf, totalOf, valuesOf } from "/assets/js/domain/context.js";
11
+ import { currentRecordCard, recordCard } from "/assets/js/domain/records.js";
12
12
  import { inputCard } from "/assets/js/domain/work.js";
13
13
  import { taskActions } from "/assets/js/views/task/actions.js";
14
14
 
@@ -30,45 +30,12 @@ export function renderOverviewPanel(panel, data, t, locale, ctx) {
30
30
  // Needs you: open InputRequests with their answer controls. Open questions
31
31
  // the bounded read did not carry are listed by reference.
32
32
  function needsCard(data, t, locale, ctx) {
33
- const task = data.task;
34
- const openInputs = data.core.attention.openInputs;
35
- const inputs = entriesOf(data.core, "input-request").filter(function (entry) { return !entry.omitted && entry.value.status === "open"; });
36
- const missing = openInputs.refs.filter(function (ref) {
37
- return !inputs.some(function (entry) { return entry.ref.refId === ref.refId; });
33
+ return currentRecordCard(data, "input-request", t, ctx, {
34
+ id: "detail-attention", title: t("overview.needsYou"), icon: "inbox",
35
+ render: function (value) {
36
+ return value.status === "open" ? inputCard(value, t, locale, ctx.answerInput) : note(value.id + " · " + value.status);
37
+ }
38
38
  });
39
- const needs = card({ title: t("overview.needsYou"), icon: "inbox", count: openInputs.count, className: "needs-card" });
40
- needs.id = "detail-attention";
41
- inputs.forEach(function (entry) { needs.body.append(inputCard(entry.value, t, locale, ctx.answerInput)); });
42
- const seen = new Set(inputs.map(function (entry) { return entry.ref.refId; }));
43
- function deferredInput(item) {
44
- seen.add(item.ref.refId);
45
- return lazyRow(item, task.id, t, ctx, {
46
- head: h("span", null, item.summary || item.ref.refId),
47
- cache: exactCache(data.viewState),
48
- render: function (value) {
49
- return value.status === "open" ? inputCard(value, t, locale, ctx.answerInput)
50
- : note(value.id + " · " + value.status);
51
- }
52
- });
53
- }
54
- missing.forEach(function (ref) {
55
- const entry = entriesOf(data.core, "input-request").find(function (entry) { return entry.ref.refId === ref.refId; });
56
- needs.body.append(deferredInput({ ref: ref, summary: entry && entry.summary }));
57
- });
58
- if (openInputs.count > inputs.length + missing.length) {
59
- let cursor;
60
- const more = button(t("overview.moreInputs"), { onClick: async function () {
61
- more.disabled = true;
62
- try {
63
- const page = await ctx.list(task.id, "input-request", { status: "open", limit: 20, cursor: cursor });
64
- page.items.forEach(function (item) { if (!seen.has(item.ref.refId)) needs.body.insertBefore(deferredInput(item), more); });
65
- cursor = page.nextCursor;
66
- more.disabled = !cursor;
67
- } catch (error) { needs.body.append(note(error.message, "bad")); more.disabled = false; }
68
- } });
69
- needs.body.append(more);
70
- }
71
- return needs;
72
39
  }
73
40
 
74
41
  function briefOf(data) {
@@ -3,38 +3,36 @@ export const TASK_RUNTIME_SCRIPT = String.raw `
3
3
  // control, open execution, open operations, usage and diagnostics. Session
4
4
  // lines, counts and raw facts are [data-slot]s the observation fills in.
5
5
  import { h } from "/assets/js/lib/dom.js";
6
- import { fill } from "/assets/js/lib/format.js";
7
- import { dot, emptyState, mono, note, timeTag } from "/assets/js/ui/primitives.js";
8
- import { card, cardDisclosure, listCard } from "/assets/js/ui/containers.js";
6
+ import { dot, button, emptyState, mono, note, timeTag } from "/assets/js/ui/primitives.js";
7
+ import { card, cardDisclosure } from "/assets/js/ui/containers.js";
9
8
  import { observabilityMetricCard } from "/assets/js/ui/metrics.js";
10
9
  import { statusBadge, tone } from "/assets/js/domain/vocab.js";
11
- import { cachedExact, entriesOf, exactCache, totalOf } from "/assets/js/domain/context.js";
12
- import { exactRow, recordCard } from "/assets/js/domain/records.js";
10
+ import { cachedExact, entriesOf, exactCache } from "/assets/js/domain/context.js";
11
+ import { currentRecordCard } from "/assets/js/domain/records.js";
13
12
  import { roleCard, runCard } from "/assets/js/domain/runs.js";
14
13
  import { controlForm } from "/assets/js/domain/taskForms.js";
15
14
 
16
15
  export function renderRuntime(panel, data, t, locale, ctx) {
17
16
  exactCache(data.viewState);
18
17
  const roleEntries = entriesOf(data.core, "role");
19
- panel.append(rolesCard(data, roleEntries, t, locale, ctx));
18
+ panel.append(rolesCard(data, t, locale, ctx));
20
19
  panel.append(controlCard(data.task, roleEntries, t, ctx));
21
20
  panel.append(runsCard(data, t, locale, ctx));
22
21
  panel.append(jobsCard(data, t, locale, ctx));
22
+ panel.append(currentRecordCard(data, "review-round", t, ctx, { id: "detail-reviews", title: t("activity.reviews") }));
23
+ panel.append(nativeCard(data, t, ctx));
23
24
  panel.append(usageCard(data, t));
24
25
  panel.append(diagnosticsCard(data.core, t));
25
26
  }
26
27
 
27
28
  // --- Roles -------------------------------------------------------------------
28
- function rolesCard(data, roleEntries, t, locale, ctx) {
29
+ function rolesCard(data, t, locale, ctx) {
29
30
  const runs = knownRuns(data);
30
- const roles = listCard({
31
- id: "detail-roles", title: t("roles.title"), icon: "user", count: totalOf(data.core, "role"), hint: t("roles.hint"),
32
- actions: h("div.chip-row", { dataset: { slot: "session-counts" } })
31
+ const roles = currentRecordCard(data, "role", t, ctx, {
32
+ id: "detail-roles", title: t("roles.title"), icon: "user",
33
+ render: function (role, result, entry) { return roleRow({ ...entry, value: role }, data, runs, t, locale, ctx); }
33
34
  });
34
- roleEntries.forEach(function (entry) {
35
- roles.list.append(entry.omitted ? recordCard(entry, data.task.id, t, ctx) : roleRow(entry, data, runs, t, locale, ctx));
36
- });
37
- if (!roleEntries.length) roles.list.append(emptyState(t("records.none")));
35
+ roles.list.prepend(h("div.chip-row", { dataset: { slot: "session-counts" } }));
38
36
  return roles;
39
37
  }
40
38
 
@@ -72,31 +70,70 @@ function controlCard(task, roleEntries, t, ctx) {
72
70
 
73
71
  // --- Execution and operations --------------------------------------------------
74
72
  function runsCard(data, t, locale, ctx) {
75
- const runEntries = entriesOf(data.core, "run");
76
- const runTotal = totalOf(data.core, "run");
77
- const runList = listCard({ id: "detail-exec", title: t("runs.title"), icon: "pulse", count: runTotal || null, hint: t("runs.hint") });
78
- if (!runEntries.length) runList.list.append(emptyState(t("runs.none"), "pulse"));
79
- runEntries.forEach(function (entry) {
80
- runList.list.append(exactRow(entry, data.task.id, t, ctx, data.viewState.exact, function (value, result) {
81
- return runCard({ ...value, execution: (result && result.execution) || entry.execution }, t, locale);
82
- }));
73
+ return currentRecordCard(data, "run", t, ctx, {
74
+ id: "detail-exec", title: t("runs.title"),
75
+ render: function (value, result) { return runCard({ ...value, execution: result && result.execution }, t, locale); }
83
76
  });
84
- if (runTotal > runEntries.length) runList.list.append(note(fill(t("runs.more"), { count: runTotal - runEntries.length })));
85
- return runList;
86
77
  }
87
78
 
88
79
  function jobsCard(data, t, locale, ctx) {
89
- const jobs = entriesOf(data.core, "job");
90
- const jobTotal = totalOf(data.core, "job");
91
- const jobList = listCard({
92
- id: "detail-operations", title: t("jobs.title"), icon: "refresh", count: jobTotal || null,
93
- hint: jobTotal ? t("jobs.hint") : null
80
+ return currentRecordCard(data, "job", t, ctx, {
81
+ id: "detail-operations", title: t("jobs.title"), icon: "refresh",
82
+ render: function (job) { return jobRecord(job, data.task, t, locale); }
94
83
  });
95
- if (!jobs.length) jobList.list.append(emptyState(t("jobs.none")));
96
- jobs.forEach(function (entry) {
97
- jobList.list.append(exactRow(entry, data.task.id, t, ctx, data.viewState.exact, function (job) { return jobRecord(job, data.task, t, locale); }));
98
- });
99
- return jobList;
84
+ }
85
+
86
+ function nativeCard(data, t, ctx) {
87
+ const element = card({ id: "detail-native-activity", title: t("activity.native"), icon: "pulse" });
88
+ let state = data.viewState.nativeActivity;
89
+ if (!state) {
90
+ state = data.viewState.nativeActivity = { key: data.core.coreCursor, items: [], page: null, pending: false, error: null };
91
+ }
92
+ const revalidate = state.page && state.key !== data.core.coreCursor;
93
+ state.key = data.core.coreCursor;
94
+ function draw() {
95
+ element.body.replaceChildren(note(t("activity.scope")));
96
+ if (!state.page) element.body.append(note(t("activity.notLoaded")));
97
+ else {
98
+ element.body.append(note(t("activity.loaded") + " " + state.items.length + " / " + state.page.total
99
+ + " · " + state.page.coverage + " · " + state.page.readAt));
100
+ if (!state.page.total) element.body.append(note(t("activity.none")));
101
+ }
102
+ state.items.forEach(function (item) {
103
+ element.body.append(h("article.record", null,
104
+ h("header.record-head", null, mono(item.roleName + " / " + item.kind), h("span.spacer"), item.status),
105
+ h("p.small", null, item.summary),
106
+ h("p.faint.small.mono-line", null, [item.nativeSessionId, item.runId, item.sourceUpdatedAt].filter(Boolean).join(" · ")),
107
+ button(t("activity.openSession"), { onClick: function () { ctx.openSession(item.roleName); } })));
108
+ });
109
+ if (state.error) element.body.append(note(state.error + " · " + t("activity.refreshHint"), "bad"));
110
+ const controls = h("div.chip-row");
111
+ if (!state.page || state.page.nextCursor) controls.append(button(t("activity.more"), {
112
+ disabled: state.pending, onClick: function () { load(!!state.page); }
113
+ }));
114
+ controls.append(button(t("activity.refresh"), { disabled: state.pending, onClick: function () { load(false); } }));
115
+ element.body.append(controls);
116
+ element.dataset.reading = String(state.pending);
117
+ }
118
+ async function load(more) {
119
+ if (state.pending) return;
120
+ state.pending = true; draw();
121
+ try {
122
+ const page = await ctx.activity(data.task.id, { limit: 20, ...(more ? { cursor: state.page.nextCursor } : {}) });
123
+ if (!more && state.page && state.page.digest === page.digest) {
124
+ // Refresh observation metadata without discarding unchanged loaded pages.
125
+ state.page = { ...page, nextCursor: state.page.nextCursor };
126
+ } else {
127
+ state.items = more ? state.items.concat(page.items) : page.items;
128
+ state.page = page;
129
+ }
130
+ state.error = null;
131
+ } catch (error) { state.error = error.message; }
132
+ finally { state.pending = false; draw(); }
133
+ }
134
+ draw();
135
+ if (revalidate) load(false);
136
+ return element;
100
137
  }
101
138
 
102
139
  function jobRecord(job, task, t, locale) {
@@ -32,6 +32,8 @@ export const ICON_PATHS = Object.freeze({
32
32
  user: ["M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", "M4 20a8 8 0 0 1 16 0"],
33
33
  eye: ["M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z", "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"],
34
34
  plus: ["M12 5v14", "M5 12h14"],
35
+ play: ["M8 5v14l11-7z"],
36
+ archive: ["M3 5h18v4H3z", "M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9", "M10 13h4"],
35
37
  target: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", "M12 12h.01"],
36
38
  flag: ["M5 21V4", "M5 4h11l-2 4 2 4H5"],
37
39
  sparkle: ["M12 3v4", "M12 17v4", "M3 12h4", "M17 12h4", "M6 6l2.5 2.5", "M15.5 15.5L18 18", "M18 6l-2.5 2.5", "M8.5 15.5L6 18"]
@@ -17,6 +17,7 @@ export const DASHBOARD_HTML = `<!doctype html>
17
17
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
18
18
  <meta name="color-scheme" content="dark light">
19
19
  <meta name="yui-web-token" content="__YUI_WEB_TOKEN__">
20
+ <script src="/assets/theme.js"></script>
20
21
  <link rel="icon" href="data:,">
21
22
  <title>Yui</title>
22
23
  <link rel="preload" href="/assets/fonts/inter-500.woff2" as="font" type="font/woff2" crossorigin>