@zq-silk/yui 2.0.0 → 2.1.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 (61) hide show
  1. package/dist/cli/commandCatalog.js +294 -286
  2. package/dist/cli/commandDiscovery.js +69 -0
  3. package/dist/cli/completion.js +22 -227
  4. package/dist/cli/dynamicCompletion.js +12 -6
  5. package/dist/cli/invocationAuthority.js +2 -2
  6. package/dist/cli/invocationRouter.js +18 -7
  7. package/dist/cli.js +71 -23
  8. package/dist/commands/capabilityCommands.js +5 -5
  9. package/dist/commands/globalRoleCommands.js +2 -1
  10. package/dist/commands/taskCommands.js +11 -0
  11. package/dist/context/taskContext.js +6 -2
  12. package/dist/kernel/accessAssessment.js +1 -0
  13. package/dist/kernel/builtinCapabilities.js +28 -1
  14. package/dist/kernel/capabilityRegistry.js +64 -19
  15. package/dist/nativeAgent/agent.js +173 -0
  16. package/dist/nativeAgent/contracts.js +1 -0
  17. package/dist/nativeAgent/demo.js +29 -0
  18. package/dist/nativeAgent/index.js +3 -0
  19. package/dist/nativeAgent/mockProvider.js +43 -0
  20. package/dist/nativeAgent/textTools.js +156 -0
  21. package/dist/nativeAgent/validation.js +95 -0
  22. package/dist/plugins/pluginService.js +98 -8
  23. package/dist/resources/projectResourceService.js +23 -1
  24. package/dist/runtime/managedIdentity.js +6 -0
  25. package/dist/surface/surfaceContributions.js +3 -0
  26. package/dist/task/taskAuthority.js +5 -10
  27. package/dist/web/assets/assetManifest.js +38 -22
  28. package/dist/web/assets/client/api.js +115 -0
  29. package/dist/web/assets/client/app.js +555 -863
  30. package/dist/web/assets/client/components.js +323 -897
  31. package/dist/web/assets/client/detail.js +333 -0
  32. package/dist/web/assets/client/dock.js +225 -0
  33. package/dist/web/assets/client/dom.js +55 -3
  34. package/dist/web/assets/client/evidence.js +172 -0
  35. package/dist/web/assets/client/format.js +25 -14
  36. package/dist/web/assets/client/forms.js +230 -0
  37. package/dist/web/assets/client/i18n.js +1020 -791
  38. package/dist/web/assets/client/overview.js +115 -0
  39. package/dist/web/assets/client/records.js +63 -0
  40. package/dist/web/assets/client/sections.js +313 -0
  41. package/dist/web/assets/client/sidebar.js +117 -0
  42. package/dist/web/assets/client/theme.js +61 -22
  43. package/dist/web/assets/icons.js +39 -0
  44. package/dist/web/assets/shell.js +124 -118
  45. package/dist/web/assets/styles/base.js +41 -0
  46. package/dist/web/assets/styles/components.js +167 -0
  47. package/dist/web/assets/styles/layout.js +49 -77
  48. package/dist/web/assets/styles/markdown.js +16 -24
  49. package/dist/web/assets/styles/responsive.js +33 -44
  50. package/dist/web/assets/styles/tokens.js +65 -89
  51. package/dist/web/assets/styles/views.js +289 -0
  52. package/dist/web/webServer.js +2 -1
  53. package/docs/release-workflow.md +23 -0
  54. package/docs/release-workflow.zh-CN.md +16 -0
  55. package/package.json +1 -1
  56. package/skills/yui-runtime/SKILL.md +26 -0
  57. package/dist/web/assets/client/taskSummary.js +0 -350
  58. package/dist/web/assets/client/taskSurface.js +0 -616
  59. package/dist/web/assets/client/view.js +0 -608
  60. package/dist/web/assets/styles/cards.js +0 -247
  61. package/dist/web/assets/styles/widgets.js +0 -168
@@ -0,0 +1,333 @@
1
+ export const DETAIL_SCRIPT = String.raw `
2
+ // Task detail: header, section tabs and the Overview section. The runtime
3
+ // observation arrives separately from the Context snapshot, so every fact it
4
+ // feeds lives in a [data-slot] that updateObservation() can redraw in place.
5
+ import { h, icon, clear } from "/assets/js/dom.js";
6
+ import { formatDateTime, formatClock, relativeTime } from "/assets/js/format.js";
7
+ import {
8
+ badge, statusBadge, label, tone, card, disclosure, richText, bulletList, emptyState,
9
+ note, button, chip, mono, inputCard, kv, dot, timeTag
10
+ } from "/assets/js/components.js";
11
+ import { entriesOf, valuesOf, recordCard } from "/assets/js/records.js";
12
+ import { renderWork, renderExecution, renderHistory, renderDetails } from "/assets/js/sections.js";
13
+
14
+ export const TABS = ["overview", "work", "execution", "history", "details"];
15
+ const TAB_ICONS = { overview: "target", work: "layers", execution: "pulse", history: "history", details: "dots" };
16
+ const SESSION_GROUPS = ["active", "waiting", "background", "quiet", "diagnostic", "stopped", "unknown", "idle"];
17
+
18
+ export function renderTaskDetail(container, data, t, locale, ctx) {
19
+ clear(container);
20
+ const task = data.task;
21
+ const core = data.core;
22
+ container.dataset.taskId = task.id;
23
+ const page = h("div.page.task-page");
24
+
25
+ // --- Header ----------------------------------------------------------------
26
+ const bar = h("div.task-bar", null,
27
+ h("button.icon-btn.back-btn", { type: "button", "aria-label": t("actions.back"), title: t("actions.back"), onclick: ctx.onBack }, icon("back")),
28
+ h("nav.crumbs", { "aria-label": t("crumbs.label") },
29
+ h("button.crumb", { type: "button", onclick: ctx.onBack }, t("crumbs.tasks")),
30
+ h("span.crumb-sep", { "aria-hidden": "true" }, "/"),
31
+ h("code.crumb-current", null, task.id)),
32
+ h("span.spacer"),
33
+ h("button.btn.btn-ghost.dock-toggle", {
34
+ type: "button", dataset: { dockToggle: "discussion" }, title: t("dock.discussion") + " · D",
35
+ onclick: function () { ctx.showDock("discussion"); }
36
+ }, icon("chat"), h("span", null, t("dock.discussion"))),
37
+ h("button.btn.btn-ghost.dock-toggle", {
38
+ type: "button", dataset: { dockToggle: "session" }, title: t("dock.session"),
39
+ onclick: function () { ctx.showDock("session"); }
40
+ }, icon("terminal"), h("span", null, t("dock.session"))));
41
+
42
+ const meta = h("div.task-meta", null,
43
+ statusBadge(t, "task", "status", task.status),
44
+ h("span", { dataset: { slot: "exec-badge" } }),
45
+ task.priority ? badge(label(t, "priority", task.priority), task.priority === "urgent" || task.priority === "high" ? "accent" : "idle") : null,
46
+ (task.tags || []).map(function (tag) { return chip("#" + tag); }),
47
+ h("span.meta-item", null, icon("clock", "icon-sm"), t("task.updated") + " ", timeTag(task.updatedAt, locale, t)),
48
+ (task.projectBindings || []).length ? h("span.meta-item", null, icon("layers", "icon-sm"),
49
+ task.projectBindings.map(function (binding) { return binding.projectId; }).join(", ")) : null);
50
+
51
+ const counts = {
52
+ work: entriesOf(core, "work-item").length,
53
+ execution: entriesOf(core, "run").length,
54
+ history: entriesOf(core, "task-decision").length + entriesOf(core, "task-milestone").length + entriesOf(core, "task-message").length
55
+ };
56
+ const tabs = h("nav.tabs", { role: "tablist", "aria-label": t("tabs.label") }, TABS.map(function (tab, index) {
57
+ return h("button.tab", {
58
+ type: "button", role: "tab", id: "tab-" + tab,
59
+ "aria-controls": "panel-" + tab,
60
+ "aria-selected": String(ctx.activeTab === tab),
61
+ tabIndex: ctx.activeTab === tab ? 0 : -1,
62
+ title: t("tabs." + tab) + " · " + (index + 1),
63
+ dataset: { tab: tab },
64
+ onclick: function () { ctx.onTab(tab); }
65
+ }, icon(TAB_ICONS[tab], "icon-sm"), h("span", null, t("tabs." + tab)),
66
+ counts[tab] ? h("span.count", null, String(counts[tab])) : null);
67
+ }));
68
+ tabs.addEventListener("keydown", function (event) {
69
+ if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
70
+ event.preventDefault();
71
+ const selected = tabs.querySelector('[aria-selected="true"]');
72
+ const index = TABS.indexOf(selected.dataset.tab);
73
+ const next = TABS[(index + (event.key === "ArrowRight" ? 1 : TABS.length - 1)) % TABS.length];
74
+ ctx.onTab(next);
75
+ const button = tabs.querySelector('[data-tab="' + next + '"]');
76
+ if (button) button.focus();
77
+ });
78
+
79
+ page.append(h("header.task-header", null, bar),
80
+ h("div.task-hero", null, h("h1.task-title", null, task.title), meta),
81
+ h("div.tabs-wrap", null, tabs));
82
+
83
+ // --- Panels ------------------------------------------------------------------
84
+ const panels = {};
85
+ TABS.forEach(function (tab) {
86
+ panels[tab] = h("section.tab-panel", { id: "panel-" + tab, role: "tabpanel", "aria-labelledby": "tab-" + tab, hidden: ctx.activeTab !== tab });
87
+ page.append(panels[tab]);
88
+ });
89
+ renderOverviewPanel(panels.overview, data, t, locale, ctx);
90
+ renderWork(panels.work, data, t, locale, ctx);
91
+ renderExecution(panels.execution, data, t, locale, ctx);
92
+ renderHistory(panels.history, data, t, locale, ctx);
93
+ renderDetails(panels.details, data, t, locale, ctx);
94
+ container.append(page);
95
+ updateObservation(container, data, t, locale, ctx);
96
+ }
97
+
98
+ export function selectTab(container, tab) {
99
+ container.querySelectorAll(".tab").forEach(function (button) {
100
+ const active = button.dataset.tab === tab;
101
+ button.setAttribute("aria-selected", String(active));
102
+ button.tabIndex = active ? 0 : -1;
103
+ });
104
+ container.querySelectorAll(".tab-panel").forEach(function (panel) { panel.hidden = panel.id !== "panel-" + tab; });
105
+ }
106
+
107
+ function renderOverviewPanel(panel, data, t, locale, ctx) {
108
+ const task = data.task;
109
+ const core = data.core;
110
+ // Now: the derived execution status (observation) plus the open questions.
111
+ const now = card({ className: "now-card" });
112
+ now.body.append(h("div", { dataset: { slot: "now" } }));
113
+ panel.append(now);
114
+
115
+ // Needs you: open InputRequests with their answer controls.
116
+ const inputs = entriesOf(core, "input-request").filter(function (entry) { return !entry.omitted && entry.value.status === "open"; });
117
+ const missing = core.attention.openInputs.refs.filter(function (ref) {
118
+ return !inputs.some(function (entry) { return entry.ref.refId === ref.refId; });
119
+ });
120
+ const inputCount = core.attention.openInputs.count;
121
+ if (inputCount) {
122
+ const needs = card({ title: t("overview.needsYou"), icon: "inbox", count: inputCount, className: "needs-card" });
123
+ needs.id = "detail-attention";
124
+ inputs.forEach(function (entry) { needs.body.append(inputCard(entry.value, t, locale, ctx.answerInput)); });
125
+ missing.forEach(function (ref) { needs.body.append(recordCard({ ref: ref, omitted: true }, task.id, t, ctx)); });
126
+ if (inputCount > inputs.length + missing.length) {
127
+ needs.body.append(note(t("overview.moreInputs"), "warn"), h("pre.code-block", null, "yui task input list " + task.id));
128
+ }
129
+ panel.append(needs);
130
+ }
131
+
132
+ const grid = h("div.card-grid");
133
+ grid.append(progressCard(data, t, locale, ctx));
134
+ const sessions = card({ title: t("sessions.title"), icon: "terminal", hint: t("sessions.hint") });
135
+ sessions.body.append(h("div", { dataset: { slot: "sessions" } }));
136
+ grid.append(sessions);
137
+ grid.append(goalCard(data, t, ctx));
138
+ grid.append(decisionsCard(data, t, locale, ctx));
139
+ panel.append(grid);
140
+ if (task.status === "draft") panel.append(planningCard(data, t, locale));
141
+ }
142
+
143
+ function progressCard(data, t, locale, ctx) {
144
+ const task = data.task;
145
+ const element = card({ title: t("progress.title"), icon: "flag" });
146
+ const working = task.status === "draft" || task.status === "active";
147
+ if (task.completionSummary) element.body.append(richText(t("progress.completion"), task.completionSummary, t, { className: "callout.tone-ok" }));
148
+ if (task.retirementSummary) element.body.append(richText(t("progress.retirement"), task.retirementSummary, t, { className: "callout" }));
149
+ if (task.archiveSummary || task.archiveReason) element.body.append(richText(t("progress.archive"), [task.archiveSummary, task.archiveReason].filter(Boolean).join("\n\n"), t, { className: "callout" }));
150
+ const briefEntry = entriesOf(data.core, "task-brief")[0];
151
+ const brief = valuesOf(data.core, "task-brief")[0];
152
+ if (brief) {
153
+ element.body.append(richText(t(working ? "progress.focus" : "progress.lastFocus"), brief.currentFocus, t));
154
+ element.body.append(richText(t(working ? "progress.report" : "progress.lastReport"), brief.leaderSummary, t, { muted: true }));
155
+ element.body.append(h("p.faint.small", null, t("progress.briefUpdated") + " " + formatDateTime(brief.updatedAt, locale)));
156
+ } else if (briefEntry) element.body.append(recordCard(briefEntry, task.id, t, ctx, { compact: true }));
157
+ else element.body.append(emptyState(t("progress.none")));
158
+ element.body.append(h("p.next-line", { dataset: { slot: "next" } }));
159
+ return element;
160
+ }
161
+
162
+ function goalCard(data, t, ctx) {
163
+ const task = data.task;
164
+ const brief = valuesOf(data.core, "task-brief")[0];
165
+ const element = card({ title: t("goal.title"), icon: "target" });
166
+ if (brief && brief.objective) element.body.append(richText(null, brief.objective, t));
167
+ if (brief && brief.boundaries && brief.boundaries.length) {
168
+ element.body.append(h("div.prose-block", null, h("h4.prose-label", null, t("goal.boundaries")), bulletList(brief.boundaries)));
169
+ }
170
+ if (brief && brief.technicalApproach) {
171
+ const approach = disclosure(t("goal.approach"), "approach");
172
+ approach.body.append(richText(null, brief.technicalApproach, t));
173
+ element.body.append(approach);
174
+ }
175
+ if (task.description) {
176
+ const requirements = disclosure(t("goal.requirements"), "requirements");
177
+ requirements.body.append(richText(null, task.description, t));
178
+ element.body.append(requirements);
179
+ }
180
+ if (!element.body.childNodes.length) element.body.append(emptyState(t("goal.none")));
181
+ return element;
182
+ }
183
+
184
+ function decisionsCard(data, t, locale, ctx) {
185
+ const entries = entriesOf(data.core, "task-decision");
186
+ const active = entries.filter(function (entry) { return !entry.omitted && entry.value.status === "active"; });
187
+ const element = card({ title: t("decisions.title"), icon: "check", count: active.length || null, hint: t("decisions.hint") });
188
+ active.slice(0, 3).forEach(function (entry) {
189
+ const value = entry.value;
190
+ element.body.append(h("article.decision", null,
191
+ h("header.decision-head", null, dot("ok"), h("strong", null, value.title), timeTag(value.createdAt, locale, t)),
192
+ value.rationale ? richText(null, value.rationale, t, { muted: true, threshold: 260 }) : null));
193
+ });
194
+ if (!active.length) element.body.append(emptyState(t("decisions.none")));
195
+ if (active.length > 3 || data.core.omitted.records || entries.some(function (entry) { return entry.omitted; })) {
196
+ element.body.append(button(t("decisions.more"), { variant: "link", icon: "chevron", onClick: function () { ctx.onTab("history"); } }));
197
+ }
198
+ return element;
199
+ }
200
+
201
+ // A Draft's Leader conversation is its planning Turn. Report the snapshot's
202
+ // real planning facts; a withheld value is reported as withheld, never as
203
+ // "not dispatched".
204
+ function planningCard(data, t, locale) {
205
+ const runEntries = entriesOf(data.core, "run");
206
+ const planning = valuesOf(data.core, "run").filter(function (run) { return run.roleName === "leader" && run.purpose === "planning"; });
207
+ const turn = planning[planning.length - 1] || null;
208
+ const withheld = turn === null && runEntries.some(function (entry) { return entry.omitted; });
209
+ const live = ((data.runtime && data.runtime.roles) || []).find(function (role) { return role.name === "leader"; }) || null;
210
+ const element = card({ title: t("planning.title"), icon: "sparkle", className: "planning-card" });
211
+ element.dataset.planning = turn ? turn.status : withheld ? "withheld" : "not-dispatched";
212
+ element.body.append(note(withheld ? t("planning.withheld") : turn === null ? t("planning.none")
213
+ : turn.status === "active" ? t("planning.open") : t("planning.settled").replace("{status}", turn.status)));
214
+ const environment = turn && turn.effective && turn.effective.executionEnvironment;
215
+ element.body.append(kv([
216
+ [t("planning.run"), turn ? mono(turn.id + " (" + turn.status + ")") : withheld ? t("planning.withheldValue") : t("common.none")],
217
+ [t("planning.providerTerminal"), (turn && turn.result && turn.result.provider && turn.result.provider.status) || t("common.notReported")],
218
+ [t("planning.conversation"), (turn && turn.result && turn.result.provider && turn.result.provider.conversationId) || t("common.notReported")],
219
+ [t("planning.liveSession"), (live && live.runtimeSession && live.runtimeSession.nativeSessionId) || t("planning.notRunning")],
220
+ [t("planning.environment"), (environment && environment.environmentRef) || t("planning.emptyEnvironment")],
221
+ [t("planning.updated"), turn ? formatDateTime((turn.result && turn.result.completedAt) || turn.updatedAt, locale) : t("common.unknown")]
222
+ ]));
223
+ return element;
224
+ }
225
+
226
+ // --- Observation slots -------------------------------------------------------
227
+ export function updateObservation(container, data, t, locale, ctx) {
228
+ const available = data.runtimeStatus === "available" && data.runtime;
229
+ const execution = available ? data.runtime.execution : null;
230
+ const execBadge = container.querySelector('[data-slot="exec-badge"]');
231
+ if (execBadge) {
232
+ clear(execBadge);
233
+ if (execution && execution.status !== data.task.status) execBadge.append(statusBadge(t, "exec", "exec.status", execution.status));
234
+ }
235
+ const now = container.querySelector('[data-slot="now"]');
236
+ if (now && !now.contains(document.activeElement)) drawNow(now, data, execution, t, locale);
237
+ const next = container.querySelector('[data-slot="next"]');
238
+ if (next) {
239
+ clear(next);
240
+ if (execution && !(execution.next.owner === "none" && execution.next.action === "none")) next.append(icon("flag", "icon-sm"), h("span", null, t("now.nextOwner") + " "),
241
+ h("strong", null, label(t, "exec.owner", execution.next.owner)), h("span", null, " · " + label(t, "exec.action", execution.next.action)));
242
+ }
243
+ const sessions = container.querySelector('[data-slot="sessions"]');
244
+ if (sessions && !sessions.contains(document.activeElement)) {
245
+ const open = sessions.querySelector("details") && sessions.querySelector("details").open;
246
+ drawSessions(sessions, available ? data.runtime.sessions : null, t, locale);
247
+ if (open && sessions.querySelector("details")) sessions.querySelector("details").open = true;
248
+ }
249
+ const raw = container.querySelector('[data-slot="runtime-raw"]');
250
+ if (raw) raw.textContent = available ? JSON.stringify({ roles: data.runtime.roles, runtimeHealth: data.runtime.runtimeHealth }, null, 2) : "";
251
+ const rawStatus = container.querySelector('[data-slot="runtime-status"]');
252
+ if (rawStatus) rawStatus.textContent = t("observation." + data.runtimeStatus) + " · " + formatDateTime(data.runtimeObservedAt, locale);
253
+ container.querySelectorAll("[data-role-status]").forEach(function (slot) {
254
+ const role = available ? (data.runtime.roles || []).find(function (item) { return item.name === slot.dataset.roleStatus; }) : null;
255
+ clear(slot);
256
+ if (role) slot.append(statusBadge(t, "role", "role", role.status));
257
+ });
258
+ }
259
+
260
+ function drawNow(slot, data, execution, t, locale) {
261
+ clear(slot);
262
+ const inputCount = data.core.attention.openInputs.count;
263
+ if (!execution) {
264
+ slot.append(h("div.now-head", null,
265
+ badge(t("observation." + data.runtimeStatus), data.runtimeStatus === "unavailable" ? "bad" : "idle", { dot: true }),
266
+ h("span.now-owner", null, inputCount ? t("now.inputs").replace("{count}", String(inputCount)) : t("now.noInputs"))));
267
+ slot.append(h("p.now-note", null, data.runtimeStatus === "waiting" ? t("now.reading") : t("now.unavailable")));
268
+ return;
269
+ }
270
+ const head = h("div.now-head", null,
271
+ statusBadge(t, "exec", "exec.status", execution.status),
272
+ execution.owner === "none" && execution.action === "none" ? null
273
+ : h("span.now-owner", null, label(t, "exec.owner", execution.owner) + " · " + label(t, "exec.action", execution.action)));
274
+ if (execution.activeRuns && execution.activeRuns.length) head.append(badge(execution.activeRuns.length + " " + t("now.activeRuns"), "info"));
275
+ if (execution.monitoring === "stopped") head.append(badge(t("now.monitoringStopped"), "warn"));
276
+ if (execution.failClosed) head.append(badge(t("now.failClosed"), "bad"));
277
+ head.append(h("span.spacer"), h("span.faint.small", { title: formatDateTime(data.runtimeObservedAt, locale) },
278
+ t("now.observed") + " " + formatClock(data.runtimeObservedAt, locale)));
279
+ slot.append(head);
280
+ if (execution.summary) slot.append(h("p.now-summary", null, execution.summary));
281
+ if (execution.reason) slot.append(h("p.now-reason", null, execution.reason));
282
+ const signals = [];
283
+ (execution.attention || []).forEach(function (item) {
284
+ signals.push(h("li.signal-row.tone-warn", null, icon("alert", "icon-sm"),
285
+ h("span.signal-kind", null, label(t, "exec.attention", item.kind)), h("span", null, item.summary),
286
+ item.owner ? h("span.faint.small", null, label(t, "exec.owner", item.owner)) : null));
287
+ });
288
+ (execution.blockers || []).forEach(function (item) {
289
+ signals.push(h("li.signal-row.tone-bad", null, icon("alert", "icon-sm"),
290
+ h("span.signal-kind", null, label(t, "exec.blocker", item.kind)), h("span", null, item.summary),
291
+ h("span.faint.small", null, label(t, "exec.owner", item.owner))));
292
+ });
293
+ const sessions = (data.runtime.sessions && data.runtime.sessions.sessions) || [];
294
+ sessions.filter(function (session) { return session.group === "waiting" && ["user", "permission"].includes(session.waitingReason); })
295
+ .forEach(function (session) {
296
+ signals.push(h("li.signal-row.tone-warn", null, icon("user", "icon-sm"),
297
+ h("span.signal-kind", null, session.roleName), h("span", null, t("now.nativeWait"))));
298
+ });
299
+ if (signals.length) slot.append(h("ul.signal-list", null, signals));
300
+ else slot.append(h("p.now-note", null, inputCount ? t("now.inputs").replace("{count}", String(inputCount)) : t("now.clear")));
301
+ }
302
+
303
+ function drawSessions(slot, observation, t, locale) {
304
+ clear(slot);
305
+ if (!observation) { slot.append(emptyState(t("sessions.unavailable"))); return; }
306
+ const chips = SESSION_GROUPS.filter(function (group) { return observation.counts[group]; }).map(function (group) {
307
+ return badge(observation.counts[group] + " " + t("session." + group), tone("session", group), { dot: true });
308
+ });
309
+ if (chips.length) slot.append(h("div.chip-row", null, chips));
310
+ if (!observation.sessions.length) { slot.append(note(t("sessions.none"))); return; }
311
+ slot.append(h("ul.session-list", null, observation.sessions.map(function (session) {
312
+ return h("li.session-row", null,
313
+ h("div.session-row-head", null, dot(tone("session", session.group)), h("strong", null, session.roleName),
314
+ h("span.faint", null, t("session." + session.group)), h("span.spacer"),
315
+ h("span.faint.small", null, session.lastActivityAt ? relativeTime(session.lastActivityAt, locale, t) : t("sessions.unobserved"))),
316
+ h("p.small", null, session.reason),
317
+ session.waitingReason ? h("p.small", null, t("sessions.waitingFor") + " " + session.waitingReason) : null);
318
+ })));
319
+ const facts = disclosure(t("sessions.facts"), "sessions");
320
+ observation.sessions.forEach(function (session) {
321
+ facts.body.append(kv([
322
+ [t("sessions.role"), session.roleName],
323
+ [t("sessions.lastActivity"), session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : t("sessions.unobserved")],
324
+ [t("sessions.inputUpdated"), formatDateTime(session.sourceUpdatedAt, locale)],
325
+ [t("sessions.operations"), session.operations.length ? session.operations.join(", ") : null],
326
+ [t("sessions.background"), session.background.length ? session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ") : null],
327
+ [t("sessions.identity"), mono([session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / "))]
328
+ ]));
329
+ });
330
+ facts.body.append(h("p.faint.small", null, t("sessions.scope") + " " + formatDateTime(observation.readAt, locale)));
331
+ slot.append(facts);
332
+ }
333
+ `;
@@ -0,0 +1,225 @@
1
+ export const DOCK_SCRIPT = String.raw `
2
+ // The session dock: the Task discussion (recorded Task messages plus the
3
+ // composer) and the live native Session terminal. The feed is a read-only
4
+ // projection and redraws on every refresh; the composer is kept per Task so a
5
+ // refresh can never discard an unsent draft or a pending receipt.
6
+ import { Terminal } from "/assets/vendor/xterm.mjs";
7
+ import { FitAddon } from "/assets/vendor/addon-fit.mjs";
8
+ import { h, icon, clear } from "/assets/js/dom.js";
9
+ import { formatShort } from "/assets/js/format.js";
10
+ import { label, chip, emptyState, richText, dot } from "/assets/js/components.js";
11
+ import { entriesOf, recordCard, recordReader } from "/assets/js/records.js";
12
+ import { messageComposer } from "/assets/js/forms.js";
13
+ import { pageToken } from "/assets/js/api.js";
14
+
15
+ export function renderDiscussion(host, data, t, locale, actions) {
16
+ const task = data.task;
17
+ const composerKey = task.id + "|" + task.status + "|" + locale;
18
+ let feed = host.querySelector(".feed");
19
+ const composerWrap = host.querySelector(".composer-wrap");
20
+ const composer = composerWrap && composerWrap.firstChild;
21
+ const keepComposer = host.dataset.taskId === task.id && composer
22
+ && (composer.dataset.key === composerKey || composer.dataset.unsent === "true" || composer.contains(document.activeElement));
23
+ const previousScroll = feed ? feed.scrollTop : 0;
24
+ const atEnd = !feed || host.dataset.taskId !== task.id || feed.scrollHeight - feed.clientHeight - feed.scrollTop < 40;
25
+ if (host.dataset.taskId !== task.id || !feed) {
26
+ clear(host);
27
+ host.append(
28
+ h("div.dock-sub", null, h("span.dock-sub-title", null, t("discussion.title")), h("span.dock-sub-note", null, t("discussion.source"))),
29
+ h("div.feed", { "aria-label": t("discussion.feed"), tabIndex: 0 }),
30
+ h("div.composer-wrap"));
31
+ feed = host.querySelector(".feed");
32
+ }
33
+ host.dataset.taskId = task.id;
34
+ if (!feed.querySelector('[data-reading="true"]')) drawFeed(feed, data, t, locale, actions);
35
+ feed.scrollTop = atEnd ? feed.scrollHeight : previousScroll;
36
+ if (!keepComposer) {
37
+ const next = messageComposer(task, t, actions);
38
+ next.dataset.key = composerKey;
39
+ host.querySelector(".composer-wrap").replaceChildren(next);
40
+ }
41
+ }
42
+
43
+ export function discussionHasUnsent(host) {
44
+ return !!host.querySelector('[data-unsent="true"]');
45
+ }
46
+
47
+ function drawFeed(feed, data, t, locale, actions) {
48
+ clear(feed);
49
+ const core = data.core;
50
+ const entries = entriesOf(core, "task-message").slice().sort(function (a, b) {
51
+ return Date.parse((a.value && a.value.createdAt) || 0) - Date.parse((b.value && b.value.createdAt) || 0);
52
+ });
53
+ if (core.omitted.records) feed.append(h("p.feed-note", null, t("discussion.omitted")));
54
+ if (!entries.length) {
55
+ feed.append(emptyState(t("discussion.empty"), "chat"));
56
+ return;
57
+ }
58
+ let lastDay = "";
59
+ entries.forEach(function (entry) {
60
+ if (entry.omitted) { feed.append(recordCard(entry, data.task.id, t, actions, { compact: true })); return; }
61
+ const value = entry.value;
62
+ const day = value.createdAt ? new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(new Date(value.createdAt)) : "";
63
+ if (day && day !== lastDay) { feed.append(h("div.feed-day", null, h("span", null, day))); lastDay = day; }
64
+ const type = (value.author && value.author.type) || "system";
65
+ const author = (value.author && value.author.roleName) || label(t, "author", type);
66
+ const message = h("article.msg.from-" + (type === "user" || type === "operator" ? "user" : type === "role" ? "role" : "system"));
67
+ message.append(h("span.msg-avatar", { "aria-hidden": "true" }, String(author).slice(0, 1).toUpperCase()));
68
+ const main = h("div.msg-main", null,
69
+ h("header.msg-head", null, h("strong", null, author),
70
+ value.intent ? chip(label(t, "intent", value.intent)) : null,
71
+ value.kind === "role-result" ? chip(label(t, "messageKind", value.kind)) : null,
72
+ h("time", { dateTime: value.createdAt || "" }, formatShort(value.createdAt, locale))),
73
+ h("div.msg-bubble", null, richText(null, value.body || "", t, { threshold: 900 })));
74
+ main.append(recordReader(entry, data.task.id, t, actions));
75
+ message.append(main);
76
+ feed.append(message);
77
+ });
78
+ }
79
+
80
+ // --- Live native Session ----------------------------------------------------------
81
+ function cssVar(name, fallback) {
82
+ const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
83
+ return value || fallback;
84
+ }
85
+
86
+ function terminalTheme() {
87
+ return {
88
+ background: cssVar("--term-bg", "#0b0c0d"),
89
+ foreground: cssVar("--term-fg", "#e6e6e8"),
90
+ cursor: cssVar("--term-cursor", "#ff7a52"),
91
+ selectionBackground: cssVar("--term-sel", "#3a2a24")
92
+ };
93
+ }
94
+
95
+ function cliFor(target) {
96
+ if (target.scope === "task") return "yui task role session inspect " + target.taskId + " " + target.roleName;
97
+ return target.roleName === "operator" ? "yui operator enter" : "yui session enter " + target.roleName;
98
+ }
99
+
100
+ export function sameTarget(left, right) {
101
+ return !!left && !!right && left.scope === right.scope && left.roleName === right.roleName
102
+ && (left.scope !== "task" || left.taskId === right.taskId);
103
+ }
104
+
105
+ export function createTerminalController(elements, t, locale, notify) {
106
+ let session = null;
107
+ let current = null;
108
+ let stateKey = "terminal.idle";
109
+ let targets = [];
110
+
111
+ function setState(key, state) {
112
+ stateKey = key;
113
+ elements.state.dataset.state = state;
114
+ elements.state.lastChild.textContent = t(key);
115
+ }
116
+ function drawTargets() {
117
+ clear(elements.targets);
118
+ targets.forEach(function (target) {
119
+ elements.targets.append(h("button.target-chip", {
120
+ type: "button",
121
+ "aria-pressed": String(sameTarget(target, current)),
122
+ title: target.scope === "task" ? target.taskId + " / " + target.roleName : t("terminal.global") + " / " + target.roleName,
123
+ onclick: function () { open(target); }
124
+ }, target.scope === "task" ? dot(target.live ? "ok" : "idle") : icon("broadcast", "icon-sm"), h("span", null, target.roleName)));
125
+ });
126
+ if (!targets.length) elements.targets.append(h("span.faint.small", null, t("terminal.noTargets")));
127
+ }
128
+ function dispose() {
129
+ if (!session) return;
130
+ const owned = session;
131
+ session = null;
132
+ owned.resizeObserver.disconnect();
133
+ owned.input.dispose();
134
+ owned.socket.close();
135
+ owned.terminal.dispose();
136
+ clear(elements.host);
137
+ }
138
+ function close() {
139
+ dispose();
140
+ current = null;
141
+ elements.empty.hidden = false;
142
+ elements.cli.textContent = "yui operator enter";
143
+ setState("terminal.idle", "idle");
144
+ drawTargets();
145
+ }
146
+ function open(target) {
147
+ dispose();
148
+ current = target;
149
+ elements.empty.hidden = true;
150
+ elements.cli.textContent = cliFor(target);
151
+ setState("terminal.connecting", "connecting");
152
+ drawTargets();
153
+ const terminal = new Terminal({
154
+ cursorBlink: true,
155
+ scrollback: 0,
156
+ convertEol: false,
157
+ fontFamily: '"JetBrains Mono","SFMono-Regular",Menlo,Consolas,monospace',
158
+ fontSize: 12.5,
159
+ lineHeight: 1.15,
160
+ theme: terminalTheme()
161
+ });
162
+ const fit = new FitAddon();
163
+ terminal.loadAddon(fit);
164
+ terminal.open(elements.host);
165
+ try { fit.fit(); } catch {}
166
+ let writable = false;
167
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
168
+ const parameters = new URLSearchParams({
169
+ scope: target.scope, role: target.roleName, cols: String(terminal.cols), rows: String(terminal.rows), token: pageToken()
170
+ });
171
+ if (target.scope === "task") parameters.set("task", target.taskId);
172
+ const socket = new WebSocket(protocol + "//" + window.location.host + "/api/terminal?" + parameters.toString());
173
+ const input = terminal.onData(function (data) {
174
+ if (writable && socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "input", data: data }));
175
+ });
176
+ const resizeObserver = new ResizeObserver(function () {
177
+ if (!session || session.terminal !== terminal || !elements.host.offsetWidth) return;
178
+ try { fit.fit(); } catch { return; }
179
+ if (socket.readyState === WebSocket.OPEN) {
180
+ socket.send(JSON.stringify({ type: "resize", columns: terminal.cols, rows: terminal.rows }));
181
+ }
182
+ });
183
+ resizeObserver.observe(elements.host);
184
+ socket.addEventListener("message", function (event) {
185
+ let message;
186
+ try { message = JSON.parse(event.data); } catch { return; }
187
+ if (message.type === "ready") {
188
+ writable = !message.readOnly;
189
+ setState(writable ? "terminal.writable" : "terminal.readOnly", writable ? "writable" : "readonly");
190
+ if (message.history && message.history.limit < message.history.target) {
191
+ const number = new Intl.NumberFormat(locale());
192
+ notify(t("terminal.historyLimited").replace("{current}", number.format(message.history.limit))
193
+ .replace("{target}", number.format(message.history.target)));
194
+ }
195
+ terminal.focus();
196
+ } else if (message.type === "data") terminal.write(message.data);
197
+ else if (message.type === "exit") setState("terminal.closed", "closed");
198
+ else if (message.type === "error") {
199
+ setState("terminal.error", "error");
200
+ terminal.writeln("\r\n" + message.message);
201
+ }
202
+ });
203
+ socket.addEventListener("close", function () {
204
+ writable = false;
205
+ if (stateKey !== "terminal.error") setState("terminal.closed", "closed");
206
+ });
207
+ socket.addEventListener("error", function () {
208
+ writable = false;
209
+ setState("terminal.error", "error");
210
+ terminal.writeln(t("terminal.errorDetail"));
211
+ });
212
+ session = { terminal, socket, input, resizeObserver };
213
+ }
214
+ drawTargets();
215
+ return {
216
+ open: open,
217
+ close: close,
218
+ current: function () { return current; },
219
+ connected: function () { return !!session; },
220
+ setTargets: function (next) { targets = next; drawTargets(); },
221
+ relabel: function () { setState(stateKey, elements.state.dataset.state); drawTargets(); },
222
+ retheme: function () { if (session) session.terminal.options.theme = terminalTheme(); }
223
+ };
224
+ }
225
+ `;
@@ -1,6 +1,9 @@
1
- export const DOM_SCRIPT = `
2
- // Tiny DOM factory shared by every client module. Kept dependency-free so it
3
- // can be imported by components and views alike.
1
+ import { ICON_PATHS } from "../icons.js";
2
+ export const DOM_SCRIPT = String.raw `
3
+ // Dependency-free DOM helpers shared by every client module.
4
+ const ICONS = ${JSON.stringify(ICON_PATHS)};
5
+ const SVG = "http://www.w3.org/2000/svg";
6
+
4
7
  export function node(tagName, className, textContent) {
5
8
  const element = document.createElement(tagName);
6
9
  if (className) element.className = className;
@@ -8,6 +11,55 @@ export function node(tagName, className, textContent) {
8
11
  return element;
9
12
  }
10
13
 
14
+ // h("button.btn.btn-primary", { type: "button", onclick }, child, "text", ...)
15
+ // Attributes: "class", "dataset", "on<event>" handlers, boolean props and
16
+ // plain attributes. Children may be nodes, strings, arrays, null or false.
17
+ export function h(spec, attrs) {
18
+ const parts = String(spec).split(".");
19
+ const element = document.createElement(parts[0] || "div");
20
+ if (parts.length > 1) element.className = parts.slice(1).join(" ");
21
+ const children = Array.prototype.slice.call(arguments, 2);
22
+ if (attrs && (attrs instanceof Node || typeof attrs !== "object" || Array.isArray(attrs))) {
23
+ children.unshift(attrs);
24
+ attrs = null;
25
+ }
26
+ if (attrs) {
27
+ Object.keys(attrs).forEach(function (key) {
28
+ const value = attrs[key];
29
+ if (value === undefined || value === null || value === false) return;
30
+ if (key === "class") element.className = (element.className ? element.className + " " : "") + value;
31
+ else if (key === "dataset") Object.assign(element.dataset, value);
32
+ else if (key.startsWith("on") && typeof value === "function") element.addEventListener(key.slice(2), value);
33
+ else if (key in element && typeof value !== "string") element[key] = value;
34
+ else element.setAttribute(key, value === true ? "" : String(value));
35
+ });
36
+ }
37
+ append(element, children);
38
+ return element;
39
+ }
40
+
41
+ function append(element, children) {
42
+ children.forEach(function (child) {
43
+ if (child === undefined || child === null || child === false) return;
44
+ if (Array.isArray(child)) append(element, child);
45
+ else element.append(child instanceof Node ? child : document.createTextNode(String(child)));
46
+ });
47
+ }
48
+
49
+ export function icon(name, className) {
50
+ const svg = document.createElementNS(SVG, "svg");
51
+ svg.setAttribute("class", "icon" + (className ? " " + className : ""));
52
+ svg.setAttribute("viewBox", "0 0 24 24");
53
+ svg.setAttribute("aria-hidden", "true");
54
+ svg.setAttribute("focusable", "false");
55
+ (ICONS[name] || []).forEach(function (d) {
56
+ const path = document.createElementNS(SVG, "path");
57
+ path.setAttribute("d", d);
58
+ svg.append(path);
59
+ });
60
+ return svg;
61
+ }
62
+
11
63
  export function clear(element) {
12
64
  element.replaceChildren();
13
65
  }