@zq-silk/yui 2.4.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 (58) hide show
  1. package/dist/cli/commandCatalog.js +4 -3
  2. package/dist/commands/taskCommands.js +48 -15
  3. package/dist/commands/taskContextCommand.js +3 -2
  4. package/dist/context/sessionBootstrapManifest.js +8 -2
  5. package/dist/context/taskContext.js +7 -4
  6. package/dist/controller/fileSchedulerStoreAdapter.js +7 -2
  7. package/dist/controller/globalInputDelivery.js +2 -4
  8. package/dist/controller/providerRetryDelivery.js +4 -2
  9. package/dist/executor/agentAdapter.js +3 -5
  10. package/dist/executor/fileRoleLaunchPlanner.js +2 -2
  11. package/dist/interaction/notificationPresentation.js +85 -0
  12. package/dist/interaction/operatorPresentation.js +1 -2
  13. package/dist/kernel/builtinCapabilities.js +2 -1
  14. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  15. package/dist/runtime/providerRetry.js +38 -0
  16. package/dist/runtime/taskSessions.js +240 -0
  17. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  18. package/dist/scheduler/taskExecutionProjection.js +78 -6
  19. package/dist/web/assets/assetManifest.js +2 -0
  20. package/dist/web/assets/client/app/taskView.js +1 -0
  21. package/dist/web/assets/client/domain/records.js +70 -2
  22. package/dist/web/assets/client/domain/runs.js +3 -2
  23. package/dist/web/assets/client/domain/vocab.js +1 -1
  24. package/dist/web/assets/client/i18n/en.js +17 -1
  25. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  26. package/dist/web/assets/client/lib/api.js +4 -1
  27. package/dist/web/assets/client/lib/theme.js +31 -10
  28. package/dist/web/assets/client/views/task/actions.js +38 -21
  29. package/dist/web/assets/client/views/task/observation.js +12 -3
  30. package/dist/web/assets/client/views/task/overview.js +7 -40
  31. package/dist/web/assets/client/views/task/runtime.js +71 -34
  32. package/dist/web/assets/shared/icons.js +2 -0
  33. package/dist/web/assets/shell/index.js +1 -0
  34. package/dist/web/assets/shell/settings.js +1 -0
  35. package/dist/web/assets/styles/views/task.js +14 -0
  36. package/dist/web/webConversation.js +2 -2
  37. package/dist/web/webServer.js +12 -0
  38. package/dist/web/webSessions.js +2 -165
  39. package/dist/web/webSnapshot.js +9 -5
  40. package/dist/web/webTaskActivity.js +54 -0
  41. package/dist/web/webTaskSurface.js +1 -0
  42. package/docs/cli-information-contract.md +1 -1
  43. package/docs/cli-information-contract.zh-CN.md +1 -1
  44. package/docs/managed-turn-and-session-runtime.md +22 -0
  45. package/docs/managed-turn-and-session-runtime.zh-CN.md +15 -0
  46. package/docs/observability/README.md +25 -1
  47. package/docs/observability/README.zh-CN.md +18 -1
  48. package/docs/provider-retry.md +11 -0
  49. package/docs/release-workflow.md +53 -0
  50. package/docs/release-workflow.zh-CN.md +40 -0
  51. package/package.json +1 -1
  52. package/skills/yui-leader/SKILL.md +5 -4
  53. package/skills/yui-leader/references/execution.md +2 -1
  54. package/skills/yui-operator/SKILL.md +3 -0
  55. package/skills/yui-reviewer/SKILL.md +4 -2
  56. package/skills/yui-runtime/SKILL.md +43 -2
  57. package/skills/yui-runtime/references/recovery.md +9 -0
  58. package/skills/yui-worker/SKILL.md +3 -2
@@ -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>
@@ -5,6 +5,7 @@ export const SETTINGS_HTML = `<!doctype html>
5
5
  <head>
6
6
  <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <meta name="yui-web-token" content="__YUI_WEB_TOKEN__">
8
+ <script src="/assets/theme.js"></script>
8
9
  <link rel="icon" href="data:,"><title>Yui · Settings</title>
9
10
  ${STYLESHEETS.map(s => `<link rel="stylesheet" href="/assets/css/${s.name}.css">`).join("\n")}
10
11
  </head>
@@ -53,6 +53,20 @@ export const TASK_STYLES = `
53
53
  .next-line .icon{color:var(--accent)}
54
54
  .next-line strong{color:var(--ink)}
55
55
 
56
+ /* Next step: a compact action bar — prompt left, actions right; the foot
57
+ (condition + receipt) is one quiet line under a hairline. */
58
+ .next-card>.card-body{gap:0;padding:14px 16px 14px 20px}
59
+ .next-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px 20px}
60
+ .next-lead{display:grid;gap:3px;flex:1 1 280px;min-width:0}
61
+ .next-kicker{color:var(--ink-4);font-size:var(--fs-2xs);font-weight:600;letter-spacing:.08em;text-transform:uppercase}
62
+ .next-prompt{color:var(--ink-2);font-size:var(--fs-sm);line-height:1.5}
63
+ .next-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;flex:none}
64
+ .next-actions .btn{height:34px;padding:0 14px}
65
+ .next-foot{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:4px 16px;margin-top:12px;padding-top:10px;border-top:1px dashed var(--line)}
66
+ .next-hint{flex:1 1 320px;min-width:0;color:var(--ink-4);font-size:var(--fs-xs);line-height:1.5}
67
+ .next-foot .receipt:empty{display:none}
68
+ @media(max-width:560px){.next-actions{flex:1 1 100%}.next-actions .btn{flex:1 1 0}}
69
+
56
70
  /* Needs you / input requests */
57
71
  .needs-card{border-color:var(--warn);box-shadow:0 0 0 3px var(--warn-soft)}
58
72
  .needs-card .card-icon{background:var(--warn-soft);color:var(--warn)}
@@ -170,7 +170,7 @@ export function createWebConversationSurface(store, surface, plan, readPage = re
170
170
  return [item];
171
171
  let messages = [];
172
172
  if (owner.scope === "task") {
173
- const ref = /^Yui Task notification: task=([A-Za-z0-9_-]+) wake=([A-Za-z0-9_-]+)\.\nRead current context: /m.exec(item.text);
173
+ const ref = /^Yui Task notification: task=([A-Za-z0-9_-]+) wake=([A-Za-z0-9_-]+)\.$/m.exec(item.text);
174
174
  if (ref?.[1] === owner.taskId) {
175
175
  const wake = store.listTaskWakes(owner.taskId).find(w => w.id === ref[2] && w.status === "consumed");
176
176
  const ids = new Set(wake?.refs?.filter(r => r.type === "message").map(r => r.id));
@@ -178,7 +178,7 @@ export function createWebConversationSurface(store, surface, plan, readPage = re
178
178
  }
179
179
  }
180
180
  else {
181
- const ref = /^Yui Global Message: role=([A-Za-z0-9_-]+) message=([A-Za-z0-9_-]+)\.\nRead your Session Context/m.exec(item.text);
181
+ const ref = /^Yui Global Message: role=([A-Za-z0-9_-]+) message=([A-Za-z0-9_-]+)\.$/m.exec(item.text);
182
182
  if (ref?.[1] === owner.roleName)
183
183
  messages = store.listGlobalRoleMessages(owner.roleName)
184
184
  .filter(m => m.id === ref[2] && m.delivery?.via === "provider" && m.deliveryTarget?.nativeSessionId === id);
@@ -5,6 +5,7 @@ import { CliError, usageError } from "../errors/cliError.js";
5
5
  import { parseTaskCatalogOptions } from "../context/taskCatalog.js";
6
6
  import { parseTaskSearchOptions, searchTaskBodies } from "../context/taskSearch.js";
7
7
  import { readTaskUsage } from "../runtime/taskUsageQuery.js";
8
+ import { readWebTaskActivity } from "./webTaskActivity.js";
8
9
  import { WebRequestRejected } from "./webMutation.js";
9
10
  import { SETTINGS_HTML } from "./assets/shell/settings.js";
10
11
  import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
@@ -484,6 +485,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
484
485
  throw new WebRequestRejected("Invalid list limit.");
485
486
  value = dependencies.surface.list(taskId, {
486
487
  store: query.get("store") ?? "",
488
+ ...(query.has("view") ? { view: query.get("view") } : {}),
487
489
  ...(query.has("status") ? { status: query.get("status") } : {}),
488
490
  ...(query.has("cursor") ? { cursor: query.get("cursor") } : {}),
489
491
  ...(limit === null ? {} : { limit: Number(limit) })
@@ -552,6 +554,16 @@ async function handleHttpRequest(request, response, store, dependencies, token,
552
554
  : buildWebTaskCatalog(store, options);
553
555
  sendJson(response, 200, snapshot, method === "HEAD");
554
556
  }
557
+ else if (/^\/api\/tasks\/[^/]+\/activity$/u.test(pathname)) {
558
+ const taskId = decodeURIComponent(pathname.split("/")[3]);
559
+ const query = new URL(request.url, "http://localhost").searchParams;
560
+ if ([...query.keys()].some(key => !["limit", "cursor"].includes(key)))
561
+ throw usageError("Invalid activity option.");
562
+ sendJson(response, 200, readWebTaskActivity(store, taskId, {
563
+ ...(query.has("limit") ? { limit: Number(query.get("limit")) } : {}),
564
+ ...(query.has("cursor") ? { cursor: query.get("cursor") } : {})
565
+ }, now()), method === "HEAD");
566
+ }
555
567
  else if (/^\/api\/tasks\/[^/]+\/usage$/u.test(pathname)) {
556
568
  const taskId = decodeURIComponent(pathname.split("/")[3]);
557
569
  const query = new URL(request.url, "http://localhost").searchParams;